Skip to content

单页应用

¥Single-Page Apps

Next.js 完全支持构建单页应用 (SPA)。

¥Next.js fully supports building Single-Page Applications (SPAs).

这包括使用预取的快速路由转换、客户端数据获取、使用浏览器 API、与第三方客户端库集成、创建静态路由等。

¥This includes fast route transitions with prefetching, client-side data fetching, using browser APIs, integrating with third-party client libraries, creating static routes, and more.

如果你有现有的 SPA,则可以迁移到 Next.js 而无需对代码进行大量更改。然后 Next.js 允许你根据需要逐步添加服务器功能。

¥If you have an existing SPA, you can migrate to Next.js without large changes to your code. Next.js then allows you to progressively add server features as needed.

什么是单页应用?

¥What is a Single-Page Application?

SPA 的定义各不相同。我们将“严格 SPA”定义为:

¥The definition of a SPA varies. We’ll define a “strict SPA” as:

  • 客户端渲染 (CSR):该应用由一个 HTML 文件(例如 index.html)提供服务。每个路由、页面转换和数据获取都由浏览器中的 JavaScript 处理。

    ¥Client-side rendering (CSR): The app is served by one HTML file (e.g. index.html). Every route, page transition, and data fetch is handled by JavaScript in the browser.

  • 无整页重新加载:客户端 JavaScript 不会为每个路由请求一个新文档,而是操纵当前页面的 DOM 并根据需要获取数据。

    ¥No full-page reloads: Rather than requesting a new document for each route, client-side JavaScript manipulates the current page’s DOM and fetches data as needed.

严格的 SPA 通常需要加载大量 JavaScript,然后页面才能交互。此外,客户端数据瀑布可能难以管理。使用 Next.js 构建 SPA 可以解决这些问题。

¥Strict SPAs often require large amounts of JavaScript to load before the page can be interactive. Further, client data waterfalls can be challenging to manage. Building SPAs with Next.js can address these issues.

为什么将 Next.js 用于 SPA?

¥Why use Next.js for SPAs?

Next.js 可以自动对你的 JavaScript 包进行代码拆分,并生成多个 HTML 入口点到不同的路由。这避免了在客户端加载不必要的 JavaScript 代码,从而减少了包大小并实现了更快的页面加载。

¥Next.js can automatically code split your JavaScript bundles, and generate multiple HTML entry points into different routes. This avoids loading unnecessary JavaScript code on the client-side, reducing the bundle size and enabling faster page loads.

next/link 组件自动 prefetches 路由,为你提供严格 SPA 的快速页面转换,但具有将应用路由状态持久化到 URL 以进行链接和共享的优势。

¥The next/link component automatically prefetches routes, giving you the fast page transitions of a strict SPA, but with the advantage of persisting application routing state to the URL for linking and sharing.

Next.js 可以作为静态站点启动,甚至可以作为严格的 SPA 启动,其中所有内容都在客户端渲染。如果你的项目发展壮大,Next.js 允许你根据需要逐步添加更多服务器功能(例如 React 服务器组件服务器操作 等)。

¥Next.js can start as a static site or even a strict SPA where everything is rendered client-side. If your project grows, Next.js allows you to progressively add more server features (e.g. React Server Components, Server Actions, and more) as needed.

示例

¥Examples

让我们探索用于构建 SPA 的常见模式以及 Next.js 如何解决它们。

¥Let's explore common patterns used to build SPAs and how Next.js solves them.

在上下文提供程序中使用 React 的 use

¥Using React’s use within a Context Provider

我们建议在父组件(或布局)中获取数据,返回 Promise,然后使用 React 的 use 在客户端组件中解开值。

¥We recommend fetching data in a parent component (or layout), returning the Promise, and then unwrapping the value in a client component with React’s use hook.

Next.js 可以在服务器上尽早开始数据提取。在这个例子中,这是根布局 - 应用的入口点。服务器可以立即开始向客户端流式传输响应。

¥Next.js can start data fetching early on the server. In this example, that’s the root layout — the entry point to your application. The server can immediately begin streaming a response to the client.

通过将数据提取“提升”到根布局,Next.js 会在应用中的任何其他组件之前尽早启动服务器上的指定请求。这消除了客户端瀑布并防止在客户端和服务器之间进行多次往返。它还可以显着提高性能,因为你的服务器更接近(理想情况下是共置)数据库所在的位置。

¥By “hoisting” your data fetching to the root layout, Next.js starts the specified requests on the server early before any other components in your application. This eliminates client waterfalls and prevents having multiple roundtrips between client and server. It can also significantly improve performance, as your server is closer (and ideally colocated) to where your database is located.

例如,更新你的根布局以调用 Promise,但不要等待它。

¥For example, update your root layout to call the Promise, but do not await it.

tsx
import { UserProvider } from './user-provider'
import { getUser } from './user' // some server-side function

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  let userPromise = getUser() // do NOT await

  return (
    <html lang="en">
      <body>
        <UserProvider userPromise={userPromise}>{children}</UserProvider>
      </body>
    </html>
  )
}
jsx
import { UserProvider } from './user-provider'
import { getUser } from './user' // some server-side function

export default function RootLayout({ children }) {
  let userPromise = getUser() // do NOT await

  return (
    <html lang="en">
      <body>
        <UserProvider userPromise={userPromise}>{children}</UserProvider>
      </body>
    </html>
  )
}

虽然你可以将 延迟并传递单个 Promise 作为客户端组件的 prop,但我们通常看到这种模式与 React 上下文提供程序配对。这使得使用自定义 React 钩子从客户端组件更轻松地进行访问成为可能。

¥While you can defer and pass a single Promise as a prop to a client component, we generally see this pattern paired with a React context provider. This enables easier access from client components with a custom React hook.

你可以将 Promise 转发给 React 上下文提供程序:

¥You can forward a Promise to the React context provider:

ts
'use client';

import { createContext, useContext, ReactNode } from 'react';

type User = any;
type UserContextType = {
  userPromise: Promise<User | null>;
};

const UserContext = createContext<UserContextType | null>(null);

export function useUser(): UserContextType {
  let context = useContext(UserContext);
  if (context === null) {
    throw new Error('useUser must be used within a UserProvider');
  }
  return context;
}

export function UserProvider({
  children,
  userPromise
}: {
  children: ReactNode;
  userPromise: Promise<User | null>;
}) {
  return (
    <UserContext.Provider value={{ userPromise }}>
      {children}
    </UserContext.Provider>
  );
}
js
'use client'

import { createContext, useContext, ReactNode } from 'react'

const UserContext = createContext(null)

export function useUser() {
  let context = useContext(UserContext)
  if (context === null) {
    throw new Error('useUser must be used within a UserProvider')
  }
  return context
}

export function UserProvider({ children, userPromise }) {
  return (
    <UserContext.Provider value={{ userPromise }}>
      {children}
    </UserContext.Provider>
  )
}

最后,你可以在任何客户端组件中调用 useUser() 自定义钩子并解开 Promise:

¥Finally, you can call the useUser() custom hook in any client component and unwrap the Promise:

tsx
'use client'

import { use } from 'react'
import { useUser } from './user-provider'

export function Profile() {
  const { userPromise } = useUser()
  const user = use(userPromise)

  return '...'
}
jsx
'use client'

import { use } from 'react'
import { useUser } from './user-provider'

export function Profile() {
  const { userPromise } = useUser()
  const user = use(userPromise)

  return '...'
}

使用 Promise 的组件(例如上面的 Profile)将被暂停。这启用了部分水合。你可以在 JavaScript 完成加载之前查看流式传输和预渲染的 HTML。

¥The component that consumes the Promise (e.g. Profile above) will be suspended. This enables partial hydration. You can see the streamed and prerendered HTML before JavaScript has finished loading.

带有 SWR 的 SPA

¥SPAs with SWR

SWR 是一个流行的用于数据获取的 React 库。

¥SWR is a popular React library for data fetching.

使用 SWR 2.3.0(和 React 19+),你可以逐步采用服务器功能以及现有的基于 SWR 的客户端数据获取代码。这是上述 use() 模式的抽象。这意味着你可以在客户端和服务器端之间移动数据获取,或同时使用两者:

¥With SWR 2.3.0 (and React 19+), you can gradually adopt server features alongside your existing SWR-based client data fetching code. This is an abstraction of the above use() pattern. This means you can move data fetching between the client and server-side, or use both:

  • 仅限客户端:useSWR(key, fetcher)

    ¥Client-only: useSWR(key, fetcher)

  • 仅限服务器:useSWR(key) + RSC 提供的数据

    ¥Server-only: useSWR(key) + RSC-provided data

  • 混合:useSWR(key, fetcher) + RSC 提供的数据

    ¥Mixed: useSWR(key, fetcher) + RSC-provided data

例如,用 <SWRConfig>fallback 封装你的应用:

¥For example, wrap your application with <SWRConfig> and a fallback:

tsx
import { SWRConfig } from 'swr'
import { getUser } from './user' // some server-side function

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <SWRConfig
      value={{
        fallback: {
          // We do NOT await getUser() here
          // Only components that read this data will suspend
          '/api/user': getUser(),
        },
      }}
    >
      {children}
    </SWRConfig>
  )
}
js
import { SWRConfig } from 'swr'
import { getUser } from './user' // some server-side function

export default function RootLayout({ children }) {
  return (
    <SWRConfig
      value={{
        fallback: {
          // We do NOT await getUser() here
          // Only components that read this data will suspend
          '/api/user': getUser(),
        },
      }}
    >
      {children}
    </SWRConfig>
  )
}

因为这是一个服务器组件,所以 getUser() 可以安全地读取 cookie、标头或与你的数据库通信。不需要单独的 API 路由。<SWRConfig> 下方的客户端组件可以使用相同的密钥调用 useSWR() 来检索用户数据。带有 useSWR 的组件代码不需要对现有的客户端获取解决方案进行任何更改。

¥Because this is a server component, getUser() can securely read cookies, headers, or talk to your database. No separate API route is needed. Client components below the <SWRConfig> can call useSWR() with the same key to retrieve the user data. The component code with useSWR does not require any changes from your existing client-fetching solution.

tsx
'use client'

import useSWR from 'swr'

export function Profile() {
  const fetcher = (url) => fetch(url).then((res) => res.json())
  // The same SWR pattern you already know
  const { data, error } = useSWR('/api/user', fetcher)

  return '...'
}
jsx
'use client'

import useSWR from 'swr'

export function Profile() {
  const fetcher = (url) => fetch(url).then((res) => res.json())
  // The same SWR pattern you already know
  const { data, error } = useSWR('/api/user', fetcher)

  return '...'
}

fallback 数据可以预渲染并包含在初始 HTML 响应中,然后立即使用 useSWR 在子组件中读取。SWR 的轮询、重新验证和缓存仍然只在客户端运行,因此它保留了你在 SPA 中依赖的所有交互性。

¥The fallback data can be prerendered and included in the initial HTML response, then immediately read in the child components using useSWR. SWR’s polling, revalidation, and caching still run client-side only, so it preserves all the interactivity you rely on for an SPA.

由于初始 fallback 数据由 Next.js 自动处理,因此你现在可以删除以前需要检查 data 是否为 undefined 的任何条件逻辑。当数据正在加载时,最近的 <Suspense> 边界将被暂停。

¥Since the initial fallback data is automatically handled by Next.js, you can now delete any conditional logic previously needed to check if data was undefined. When the data is loading, the closest <Suspense> boundary will be suspended.

SWRRSCRSC + SWR
SSR 数据
SSR 时流式传输
重复请求删除
客户端功能

带有 React Query 的 SPA

¥SPAs with React Query

你可以在客户端和服务器上将 React Query 与 Next.js 一起使用。这使你能够构建严格的 SPA,并利用 Next.js 与 React Query 配对的服务器功能。

¥You can use React Query with Next.js on both the client and server. This enables you to build both strict SPAs, as well as take advantage of server features in Next.js paired with React Query.

React Query 文档 中了解更多信息。

¥Learn more in the React Query documentation.

仅在浏览器

¥Rendering components only in the browser

客户端组件在 next build 期间是 prerendered。如果你想禁用客户端组件的预渲染并仅在浏览器环境中加载它,则可以使用 next/dynamic

¥Client components are prerendered during next build. If you want to disable prerendering for a client component and only load it in the browser environment, you can use next/dynamic:

jsx
import dynamic from 'next/dynamic'

const ClientOnlyComponent = dynamic(() => import('./component'), {
  ssr: false,
})

这对于依赖浏览器 API(如 windowdocument)的第三方库非常有用。你还可以添加 useEffect 来检查这些 API 是否存在,如果不存在,则返回 null 或将预渲染的加载状态。

¥This can be useful for third-party libraries that rely on browser APIs like window or document. You can also add a useEffect that checks for the existence of these APIs, and if they do not exist, return null or a loading state which would be prerendered.

客户端上的浅路由

¥Shallow routing on the client

如果你要从严格的 SPA(如 创建 React 应用Vite)迁移,则可能已有浅路由来更新 URL 状态的代码。这对于在应用中的视图之间手动转换非常有用,而无需使用默认的 Next.js 文件系统路由。

¥If you are migrating from a strict SPA like Create React App or Vite, you might have existing code which shallow routes to update the URL state. This can be useful for manual transitions between views in your application without using the default Next.js file-system routing.

Next.js 允许你使用原生 window.history.pushStatewindow.history.replaceState 方法来更新浏览器的历史堆栈,而无需重新加载页面。

¥Next.js allows you to use the native window.history.pushState and window.history.replaceState methods to update the browser's history stack without reloading the page.

pushStatereplaceState 调用集成到 Next.js 路由中,允许你与 usePathnameuseSearchParams 同步。

¥pushState and replaceState calls integrate into the Next.js Router, allowing you to sync with usePathname and useSearchParams.

tsx
'use client'

import { useSearchParams } from 'next/navigation'

export default function SortProducts() {
  const searchParams = useSearchParams()

  function updateSorting(sortOrder: string) {
    const urlSearchParams = new URLSearchParams(searchParams.toString())
    urlSearchParams.set('sort', sortOrder)
    window.history.pushState(null, '', `?${urlSearchParams.toString()}`)
  }

  return (
    <>
      <button onClick={() => updateSorting('asc')}>Sort Ascending</button>
      <button onClick={() => updateSorting('desc')}>Sort Descending</button>
    </>
  )
}
jsx
'use client'

import { useSearchParams } from 'next/navigation'

export default function SortProducts() {
  const searchParams = useSearchParams()

  function updateSorting(sortOrder) {
    const urlSearchParams = new URLSearchParams(searchParams.toString())
    urlSearchParams.set('sort', sortOrder)
    window.history.pushState(null, '', `?${urlSearchParams.toString()}`)
  }

  return (
    <>
      <button onClick={() => updateSorting('asc')}>Sort Ascending</button>
      <button onClick={() => updateSorting('desc')}>Sort Descending</button>
    </>
  )
}

了解有关 路由和导航 在 Next.js 中的工作原理的更多信息。

¥Learn more about how routing and navigation work in Next.js.

在客户端组件中使用服务器操作

¥Using Server Actions in client components

你可以在使用客户端组件的同时逐步采用服务器操作。这允许你删除样板代码来调用 API 路由,而是使用 React 功能(如 useActionState)来处理加载和错误状态。

¥You can progressively adopt Server Actions while still using client components. This allows you to remove boilerplate code to call an API route, and instead use React features like useActionState to handle loading and error states.

例如,创建你的第一个服务器操作:

¥For example, create your first Server Action:

tsx
'use server'

export async function create() {}
js
'use server'

export async function create() {}

你可以从客户端导入和使用服务器操作,类似于调用 JavaScript 函数。你不需要手动创建 API 端点:

¥You can import and use a Server Action from the client, similar to calling a JavaScript function. You do not need to create an API endpoint manually:

tsx
'use client'

import { create } from './actions'

export function Button() {
  return <button onClick={() => create()}>Create</button>
}
jsx
'use client'

import { create } from './actions'

export function Button() {
  return <button onClick={() => create()}>Create</button>
}

了解有关 使用服务器操作改变数据 的更多信息。

¥Learn more about mutating data with Server Actions.

静态导出(可选)

¥Static export (optional)

Next.js 还支持生成完整的 静态站点。与严格的 SPA 相比,这具有一些优势:

¥Next.js also supports generating a fully static site. This has some advantages over strict SPAs:

  • 自动代码拆分:Next.js 不会为每个路由生成一个 HTML 文件,而是会为每个路由生成一个 HTML 文件,因此你的访问者无需等待客户端 JavaScript 包即可更快地获取内容。

    ¥Automatic code-splitting: Instead of shipping a single index.html, Next.js will generate an HTML file per route, so your visitors get the content faster without waiting for the client JavaScript bundle.

  • 改善用户体验:你将获得每条路由的完整渲染页面,而不是所有路由的最小框架。当用户在客户端导航时,转换保持即时和类似 SPA。

    ¥Improved user experience: Instead of a minimal skeleton for all routes, you get fully rendered pages for each route. When users navigate client side, transitions remain instant and SPA-like.

要启用静态导出,请更新你的配置:

¥To enable a static export, update your configuration:

ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  output: 'export',
}

export default nextConfig

运行 next build 后,Next.js 将为你的应用创建一个包含 HTML/CSS/JS 资源的 out 文件夹。

¥After running next build, Next.js will create an out folder with the HTML/CSS/JS assets for your application.

注意:Next.js 服务器功能不支持静态导出。了解更多

¥Note: Next.js server features are not supported with static exports. Learn more.

将现有项目迁移到 Next.js

¥Migrating existing projects to Next.js

你可以按照我们的指南逐步迁移到 Next.js:

¥You can incrementally migrate to Next.js by following our guides:

如果你已经在使用带有 Pages Router 的 SPA,则可以了解如何 逐步采用 App Router

¥If you are already using a SPA with the Pages Router, you can learn how to incrementally adopt the App Router.

Next.js v15.2 中文网 - 粤ICP备13048890号