跳到内容

如何使用 Next.js 构建单页应用程序

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

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

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

什么是单页应用程序?

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

  • 客户端渲染 (CSR):应用程序由一个 HTML 文件(例如 `index.html`)提供服务。每个路由、页面转换和数据获取都由浏览器中的 JavaScript 处理。
  • 无整页重新加载:客户端 JavaScript 不会为每个路由请求一个新文档,而是根据需要操纵当前页面的 DOM 并获取数据。

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

为什么要用 Next.js 构建 SPA?

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

next/link 组件自动预取路由,为您提供严格 SPA 的快速页面转换,同时具有将应用程序路由状态持久化到 URL 以供链接和共享的优势。

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

示例

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

在 Context Provider 中使用 React 的 use

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

Next.js 可以及早在服务器上开始数据获取。在此示例中,它是根布局——您应用程序的入口点。服务器可以立即开始向客户端流式传输响应。

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

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

app/layout.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>
  )
}

虽然您可以推迟并传递单个 Promise 作为 props 给客户端组件,但我们通常会看到这种模式与 React context provider 配对使用。这使得客户端组件使用自定义 React Hook 能够更轻松地访问。

您可以将 Promise 转发到 React context provider

app/user-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>
  );
}

最后,您可以在任何客户端组件中调用 useUser() 自定义 Hook 并解包 Promise

app/profile.tsx
'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。

使用 SWR 的 SPA

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

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

  • 仅客户端: useSWR(key, fetcher)
  • 仅服务器: useSWR(key) + RSC 提供的数据
  • 混合: useSWR(key, fetcher) + RSC 提供的数据

例如,使用 <SWRConfig>fallback 包装您的应用程序

app/layout.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>
  )
}

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

app/profile.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 '...'
}

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

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

SWRRSCRSC + SWR
SSR 数据
SSR 期间的流式传输
请求去重
客户端功能

使用 React Query 的 SPA

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

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

仅在浏览器中渲染组件

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

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

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

客户端浅路由

如果您正在从像 Create React AppVite 这样的严格 SPA 迁移,您可能已经有浅路由代码来更新 URL 状态。这对于在应用程序视图之间进行手动转换非常有用,**而无需**使用默认的 Next.js 文件系统路由。

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

pushStatereplaceState 调用与 Next.js 路由器集成,允许您与 usePathnameuseSearchParams 同步。

'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>
    </>
  )
}

详细了解 Next.js 中路由和导航的工作原理。

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

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

例如,创建您的第一个服务器动作

app/actions.ts
'use server'
 
export async function create() {}

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

app/button.tsx
'use client'
 
import { create } from './actions'
 
export function Button() {
  return <button onClick={() => create()}>Create</button>
}

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

静态导出(可选)

Next.js 还支持生成完全静态站点。这比严格的 SPA 有一些优势

  • 自动代码拆分:Next.js 将为每个路由生成一个 HTML 文件,而不是交付单个 index.html,因此您的访问者可以更快地获取内容,而无需等待客户端 JavaScript 包。
  • 改进的用户体验: 不再是所有路由的最小骨架,您将获得每个路由的完全渲染页面。当用户在客户端导航时,过渡仍然即时且类似于 SPA。

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

next.config.ts
import type { NextConfig } from 'next'
 
const nextConfig: NextConfig = {
  output: 'export',
}
 
export default nextConfig

运行 next build 后,Next.js 将创建一个 out 文件夹,其中包含您应用程序的 HTML/CSS/JS 资产。

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

将现有项目迁移到 Next.js

您可以通过遵循我们的指南逐步迁移到 Next.js

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