默认情况下,布局和页面是 Server Components,这允许你在服务器上获取数据并渲染部分 UI,可以选择缓存结果,并将其流式传输到客户端。当你需要交互性或浏览器 API 时,可以使用 Client Components 来分层添加功能。
本页面将解释 Next.js 中 Server 和 Client Components 的工作原理以及何时使用它们,并提供如何在应用程序中将它们组合起来的示例。
客户端和服务器环境具有不同的能力。Server 和 Client Components 允许你根据用例在各自环境中运行逻辑。
当你需要以下情况时,请使用 Client Components:
onClick、onChange。useEffect。localStorage、window、Navigator.geolocation 等。当你需要以下情况时,请使用 Server Components:
例如,<Page> 组件是一个 Server Component,它获取有关帖子的数据,并将其作为 props 传递给处理客户端交互的 <LikeButton>。
import LikeButton from '@/app/ui/like-button'
import { getPost } from '@/lib/data'
export default async function Page({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
const post = await getPost(id)
return (
<div>
<main>
<h1>{post.title}</h1>
{/* ... */}
<LikeButton likes={post.likes} />
</main>
</div>
)
}import LikeButton from '@/app/ui/like-button'
import { getPost } from '@/lib/data'
export default async function Page({ params }) {
const post = await getPost(params.id)
return (
<div>
<main>
<h1>{post.title}</h1>
{/* ... */}
<LikeButton likes={post.likes} />
</main>
</div>
)
}'use client'
import { useState } from 'react'
export default function LikeButton({ likes }: { likes: number }) {
// ...
}'use client'
import { useState } from 'react'
export default function LikeButton({ likes }) {
// ...
}在服务器上,Next.js 使用 React 的 API 来协调渲染。渲染工作根据各个路由段(布局和页面)分成块:
什么是 React Server Component Payload (RSC)?
RSC Payload 是已渲染的 React Server Components 树的紧凑二进制表示。它被客户端的 React 用于更新浏览器的 DOM。RSC Payload 包含:
- Server Components 的渲染结果
- 应该渲染 Client Components 的占位符及其 JavaScript 文件引用
- 从 Server Component 传递到 Client Component 的任何 props
然后,在客户端:
什么是注水(hydration)?
注水是 React 将事件处理程序附加到 DOM 的过程,以使静态 HTML 具有交互性。
在后续导航中:
你可以通过在文件顶部、import 语句上方添加 "use client" 指令来创建 Client Component。
'use client'
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>{count} likes</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
)
}'use client'
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>{count} likes</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
)
}"use client" 用于声明 Server 和 Client 模块图(树)之间的边界。
一旦文件被标记为 "use client",它的所有导入和子组件都被视为客户端 bundle 的一部分。这意味着你无需将该指令添加到每一个用于客户端的组件中。
为了减小客户端 JavaScript bundle 的大小,请将 'use client' 添加到特定的交互式组件中,而不是将 UI 的大部分标记为 Client Components。
例如,<Layout> 组件主要包含静态元素,如 logo 和导航链接,但包含一个交互式搜索栏。<Search /> 是交互式的,需要是一个 Client Component,但是布局的其余部分可以保持为 Server Component。
// Client Component
import Search from './search'
// Server Component
import Logo from './logo'
// Layout is a Server Component by default
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<nav>
<Logo />
<Search />
</nav>
<main>{children}</main>
</>
)
}// Client Component
import Search from './search'
// Server Component
import Logo from './logo'
// Layout is a Server Component by default
export default function Layout({ children }) {
return (
<>
<nav>
<Logo />
<Search />
</nav>
<main>{children}</main>
</>
)
}'use client'
export default function Search() {
// ...
}'use client'
export default function Search() {
// ...
}你可以使用 props 将数据从 Server Components 传递到 Client Components。
import LikeButton from '@/app/ui/like-button'
import { getPost } from '@/lib/data'
export default async function Page({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
const post = await getPost(id)
return <LikeButton likes={post.likes} />
}import LikeButton from '@/app/ui/like-button'
import { getPost } from '@/lib/data'
export default async function Page({ params }) {
const post = await getPost(params.id)
return <LikeButton likes={post.likes} />
}'use client'
export default function LikeButton({ likes }: { likes: number }) {
// ...
}'use client'
export default function LikeButton({ likes }) {
// ...
}或者,你可以使用 use Hook 从 Server Component 向 Client Component 流式传输数据。请参阅示例。
须知:传递给 Client Components 的 props 需要是 React 可序列化 的。
你可以将 Server Components 作为 props 传递给 Client Component。这允许你在 Client Components 中视觉上嵌套服务器渲染的 UI。
一种常见模式是使用 children 在 <ClientComponent> 中创建一个_插槽_。例如,一个在服务器上获取数据的 <Cart> 组件,放置在一个使用客户端状态来切换可见性的 <Modal> 组件内部。
'use client'
export default function Modal({ children }: { children: React.ReactNode }) {
return <div>{children}</div>
}'use client'
export default function Modal({ children }) {
return <div>{children}</div>
}然后,在父级 Server Component(例如 <Page>)中,你可以将 <Cart> 作为 <Modal> 的子组件传递:
import Modal from './ui/modal'
import Cart from './ui/cart'
export default function Page() {
return (
<Modal>
<Cart />
</Modal>
)
}import Modal from './ui/modal'
import Cart from './ui/cart'
export default function Page() {
return (
<Modal>
<Cart />
</Modal>
)
}在这种模式下,所有 Server Components(包括作为 props 传递的组件)都将在服务器上提前渲染。生成的 RSC payload 将包含客户端组件在组件树中应渲染位置的引用。
React context 通常用于共享全局状态,例如当前主题。然而,Server Components 不支持 React context。
要使用 context,请创建一个接受 children 的 Client Component:
'use client'
import { createContext } from 'react'
export const ThemeContext = createContext({})
export default function ThemeProvider({
children,
}: {
children: React.ReactNode
}) {
return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
}'use client'
import { createContext } from 'react'
export const ThemeContext = createContext({})
export default function ThemeProvider({ children }) {
return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
}然后,将其导入到 Server Component(例如 layout)中:
import ThemeProvider from './theme-provider'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
)
}import ThemeProvider from './theme-provider'
export default function RootLayout({ children }) {
return (
<html>
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
)
}现在,你的 Server Component 将能够直接渲染你的提供者,并且应用程序中的所有其他 Client Components 都将能够消费此 context。
须知:你应该尽可能深地在树中渲染提供者——请注意
ThemeProvider仅包裹{children}而不是整个<html>文档。这使得 Next.js 更容易优化 Server Components 的静态部分。
当使用依赖于客户端特性的第三方组件时,你可以将其包裹在一个 Client Component 中,以确保它按预期工作。
例如,<Carousel /> 可以从 acme-carousel 包中导入。该组件使用 useState,但它还没有 "use client" 指令。
如果你在 Client Component 中使用 <Carousel />,它将按预期工作:
'use client'
import { useState } from 'react'
import { Carousel } from 'acme-carousel'
export default function Gallery() {
const [isOpen, setIsOpen] = useState(0)
return (
<div>
<button onClick={() => setIsOpen(true)}>View pictures</button>
{/* Works, since Carousel is used within a Client Component */}
{isOpen && <Carousel />}
</div>
)
}'use client'
import { useState } from 'react'
import { Carousel } from 'acme-carousel'
export default function Gallery() {
const [isOpen, setIsOpen] = useState(false)
return (
<div>
<button onClick={() => setIsOpen(true)}>View pictures</button>
{/* Works, since Carousel is used within a Client Component */}
{isOpen && <Carousel />}
</div>
)
}然而,如果你尝试在 Server Component 中直接使用它,你将会看到一个错误。这是因为 Next.js 不知道 <Carousel /> 正在使用仅客户端特性。
要解决此问题,你可以将依赖于仅客户端特性的第三方组件包裹在你自己的 Client Components 中:
'use client'
import { Carousel } from 'acme-carousel'
export default Carousel'use client'
import { Carousel } from 'acme-carousel'
export default Carousel现在,你可以直接在 Server Component 中使用 <Carousel />:
import Carousel from './carousel'
export default function Page() {
return (
<div>
<p>View pictures</p>
{/* Works, since Carousel is a Client Component */}
<Carousel />
</div>
)
}import Carousel from './carousel'
export default function Page() {
return (
<div>
<p>View pictures</p>
{/* Works, since Carousel is a Client Component */}
<Carousel />
</div>
)
}给库作者的建议
如果你正在构建一个组件库,请将
"use client"指令添加到依赖于仅客户端特性的入口点。这允许你的用户将组件导入到 Server Components 中,而无需创建包装器。值得注意的是,某些打包器可能会移除
"use client"指令。你可以在 React Wrap Balancer 和 Vercel Analytics 仓库中找到如何配置 esbuild 以包含"use client"指令的示例。
JavaScript 模块可以在 Server 和 Client Components 模块之间共享。这意味着可能会意外地将仅服务器代码导入到客户端。例如,考虑以下函数:
export async function getData() {
const res = await fetch('https://external-service.com/data', {
headers: {
authorization: process.env.API_KEY,
},
})
return res.json()
}export async function getData() {
const res = await fetch('https://external-service.com/data', {
headers: {
authorization: process.env.API_KEY,
},
})
return res.json()
}此函数包含一个 API_KEY,该密钥绝不应暴露给客户端。
在 Next.js 中,只有以 NEXT_PUBLIC_ 为前缀的环境变量才会包含在客户端 bundle 中。如果变量没有前缀,Next.js 会将其替换为空字符串。
因此,即使 getData() 可以在客户端导入和执行,它也不会按预期工作。
为了防止在 Client Components 中意外使用,你可以使用 server-only 包。
然后,将该包导入到包含仅服务器代码的文件中:
import 'server-only'
export async function getData() {
const res = await fetch('https://external-service.com/data', {
headers: {
authorization: process.env.API_KEY,
},
})
return res.json()
}现在,如果你尝试将此模块导入到 Client Component 中,将会出现构建时错误。
相应的 client-only 包 可用于标记包含仅客户端逻辑的模块,例如访问 window 对象的代码。
在 Next.js 中,安装 server-only 或 client-only 是可选的。但是,如果你的 linting 规则标记了多余的依赖项,你可以安装它们以避免问题。
npm install server-onlyyarn add server-onlypnpm add server-onlybun add server-onlyNext.js 内部处理 server-only 和 client-only 导入,以便在模块用于错误环境时提供更清晰的错误消息。Next.js 不会使用这些 NPM 包中的内容。
Next.js 还为 server-only 和 client-only 提供了自己的类型声明,适用于 noUncheckedSideEffectImports 处于活动状态的 TypeScript 配置。