Next.js 完全支持构建单页应用 (SPA)。
这包括通过预取实现快速路由过渡、客户端数据获取、使用浏览器 API、集成第三方客户端库、创建静态路由等。
如果您有一个现有的 SPA,您可以无需对代码进行大量修改即可迁移到 Next.js。Next.js 允许您根据需要逐步添加服务器功能。
SPA 的定义各不相同。我们将“严格意义上的 SPA”定义为:
index.html)提供服务。每个路由、页面过渡和数据获取都由浏览器中的 JavaScript 处理。严格意义上的 SPA 通常需要加载大量 JavaScript 才能使页面具有交互性。此外,客户端数据瀑布流可能难以管理。使用 Next.js 构建 SPA 可以解决这些问题。
Next.js 可以自动进行 JavaScript 包的代码分割,并为不同路由生成多个 HTML 入口点。这避免了在客户端加载不必要的 JavaScript 代码,从而减少了包大小并加快了页面加载速度。
next/link 组件自动 预取 路由,为您提供严格意义上的 SPA 所具有的快速页面过渡,但又具有将应用程序路由状态持久化到 URL 以便链接和共享的优点。
Next.js 可以从静态站点甚至是一个所有内容都在客户端渲染的严格意义上的 SPA 开始。如果您的项目不断发展,Next.js 允许您根据需要逐步添加更多服务器功能(例如 React Server Components、Server Actions 等)。
接下来,我们将探讨构建 SPA 的常见模式以及 Next.js 如何解决这些问题。
use我们建议在父组件(或布局)中获取数据,返回 Promise,然后使用 React 的 use 钩子 在客户端组件中解包该值。
Next.js 可以在服务器上尽早开始数据获取。在此示例中,即根布局——您应用程序的入口点。服务器可以立即开始向客户端流式传输响应。
通过将数据获取“提升”到根布局,Next.js 会在应用程序中的任何其他组件之前,尽早地在服务器上启动指定的请求。这消除了客户端瀑布流,并防止了客户端和服务器之间进行多次往返。这还可以显著提高性能,因为您的服务器更接近(理想情况下是与数据库并置)数据库所在的位置。
例如,更新您的根布局以调用 Promise,但不要 await 它。
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>
)
}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 Hook 更轻松地访问。
您可以将 Promise 转发给 React 上下文提供程序:
'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>
);
}'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:
'use client'
import { use } from 'react'
import { useUser } from './user-provider'
export function Profile() {
const { userPromise } = useUser()
const user = use(userPromise)
return '...'
}'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 是一个流行的数据获取 React 库。
借助 SWR 2.3.0(和 React 19+),您可以将服务器功能与现有的基于 SWR 的客户端数据获取代码一起逐步采用。这是上述 use() 模式的抽象。这意味着您可以在客户端和服务器端之间移动数据获取,或者两者都使用:
useSWR(key, fetcher)useSWR(key) + RSC-provided datauseSWR(key, fetcher) + RSC-provided data例如,使用 <SWRConfig> 和 fallback 包装您的应用程序:
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>
)
}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() 可以安全地读取 cookies、headers,或与您的数据库通信。不需要单独的 API 路由。<SWRConfig> 下的客户端组件可以使用相同的 key 调用 useSWR() 来检索用户数据。使用 useSWR 的组件代码不需要对您现有的客户端数据获取解决方案进行任何更改。
'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 '...'
}'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> 边界将被挂起。
| SWR | RSC | RSC + SWR | |
|---|---|---|---|
| SSR 数据 | |||
| SSR 期间流式传输 | |||
| 去重请求 | |||
| 客户端功能 |
您可以在客户端和服务器上将 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,
})这对于依赖 window 或 document 等浏览器 API 的第三方库很有用。您还可以添加一个 useEffect 来检查这些 API 是否存在,如果它们不存在,则返回 null 或一个将被预渲染的加载状态。
如果您正在从 Create React App 或 Vite 等严格意义上的 SPA 迁移,您可能拥有通过浅层路由更新 URL 状态的现有代码。这对于在您的应用程序中进行视图之间的手动过渡很有用,而无需使用 Next.js 默认的文件系统路由。
Next.js 允许您使用原生的 window.history.pushState 和 window.history.replaceState 方法来更新浏览器的历史堆栈,而无需重新加载页面。
pushState 和 replaceState 调用集成到 Next.js 路由器中,允许您与 usePathname 和 useSearchParams 同步。
'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 中的路由和导航 的工作原理。
您可以逐步采用服务器 Actions,同时仍在使用客户端组件。这使您能够消除调用 API 路由的样板代码,并转而使用 useActionState 等 React 功能来处理加载和错误状态。
例如,创建您的第一个服务器 Action:
'use server'
export async function create() {}'use server'
export async function create() {}您可以从客户端导入和使用服务器 Action,类似于调用 JavaScript 函数。您无需手动创建 API 端点:
'use client'
import { create } from './actions'
export function Button() {
return <button onClick={() => create()}>Create</button>
}'use client'
import { create } from './actions'
export function Button() {
return <button onClick={() => create()}>Create</button>
}了解有关 使用 Server Actions 更改数据 的更多信息。
Next.js 也支持生成完全的 静态站点。这比严格意义上的 SPA 具有一些优势:
index.html 文件,而是为每个路由生成一个 HTML 文件,因此您的访问者可以更快地获取内容,而无需等待客户端 JavaScript 包。要启用静态导出,请更新您的配置:
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:
如果您已经在使用 Pages Router 的 SPA,您可以了解如何 逐步采用 App Router。