跳到主要内容

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

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

这包括带有预取的快速路由转换、客户端数据获取、使用浏览器 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' // 一些服务器端函数

export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
let userPromise = getUser() // 不要等待

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

虽然你可以延迟并传递单个 Promise作为 prop 给客户端组件,但我们通常看到这种模式与 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' // 一些服务器端函数

export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<SWRConfig
value={{
fallback: {
// 我们在这里不等待 getUser()
// 只有读取此数据的组件才会暂停
'/api/user': getUser(),
},
}}
>
{children}
</SWRConfig>
)
}

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

app/profile.tsx
'use client'

import useSWR from 'swr'

export function Profile() {
const fetcher = (url) => fetch(url).then((res) => res.json())
// 你已经熟悉的 SWR 模式
const { data, error } = useSWR('/api/user', fetcher)

return '...'
}

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

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

SWRRSCRSC + SWR
SSR data
Streaming while SSR
Deduplicate requests
Client-side features

使用 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,
})

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

客户端的浅层路由

如果你从严格 SPA(如 Create React AppVite)迁移,你可能有不使用默认 Next.js 文件系统路由的现有代码来浅层路由以更新 URL 状态。这对于应用程序中视图之间的手动转换很有用。

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

pushStatereplaceState 调用集成到 Next.js Router 中,允许你与 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>
</>
)
}
'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 中路由和导航如何工作。

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

你可以在仍使用客户端组件的同时逐步采用服务器操作。这允许你删除调用 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 有一些优势:

  • 自动代码分割:不是发送单个 index.html,Next.js 将为每个路由生成一个 HTML 文件,因此你的访问者无需等待客户端 JavaScript 包即可更快地获得内容。
  • 改进的用户体验:不是所有路由的最小骨架,你为每个路由获得完全渲染的页面。当用户客户端导航时,转换保持即时和 SPA 样。

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

next.config.ts
import type { NextConfig } from 'next'

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

export default nextConfig

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

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

将现有项目迁移到 Next.js

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

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