Server 和 Client 组件
默认情况下,布局和页面是 Server Components,这使你可以在服务器上获取数据并渲染 UI 的各个部分,可选地缓存结果,并将其流式传输到客户端。当你需要交互性或浏览器 API 时,可以使用 Client Components 来分层添加功能。
本页面解释了 Server 和 Client 组件在 Next.js 中的工作原理以及何时使用它们,并提供了如何在应用程序中将它们组合在一起的示例。
何时使用 Server 和 Client 组件?
客户端和服务器环境具有不同的能力。Server 和 Client 组件允许你根据用例在每个环境中运行逻辑。
当你需要以下情况时使用 Client Components:
- 状态和事件处理程序。例如
onClick、onChange。 - 生命周期逻辑。例如
useEffect。 - 仅浏览器 API。例如
localStorage、window、Navigator.geolocation等。 - 自定义 hooks。
当你需要以下情况时使用 Server Components:
- 从数据库或靠近数据源的 API 获取数据。
- 使用 API 密钥、令牌和其他密钥而不将它们暴露给客户端。
- 减少发送到浏览器的 JavaScript 量。
- 改善 First Contentful Paint (FCP),并逐步将内容流式传输到客户端。
例如,<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>
)
}'use client'
import { useState } from 'react'
export default function LikeButton({ likes }: { likes: number }) {
// ...
}Server 和 Client 组件在 Next.js 中如何工作?
在服务器上
在服务器上,Next.js 使用 React 的 API 来编排渲染。渲染工作按各个路由段(布局和页面)被分割成块:
- Server Components 被渲染成一种称为 React Server Component Payload(RSC Payload)的特殊数据格式。
- Client Components 和 RSC Payload 用于预渲染 HTML。
什么是 React Server Component Payload(RSC)?
RSC Payload 是已渲染的 React Server Components 树的紧凑二进制表示。它被 React 在客户端用于更新浏览器的 DOM。RSC Payload 包含:
- Server Components 的渲染结果
- Client Components 应该渲染的位置的占位符以及对其 JavaScript 文件的引用
- 从 Server Component 传递到 Client Component 的任何 props
在客户端(首次加载)
然后,在客户端:
- HTML 用于立即向用户显示路由的快速非交互式预览。
- RSC Payload 用于协调 Client 和 Server Component 树。
- JavaScript 用于水合 Client Components 并使应用程序具有交互性。
什么是水合(hydration)?
水合是 React 将事件处理程序附加到 DOM 的过程,以使静态 HTML 具有交互性。
后续导航
在后续导航中:
- RSC Payload 被预取并缓存以实现即时导航。
- Client Components 完全在客户端渲染,无需服务器渲染的 HTML。
示例
使用 Client Components
你可以通过在文件顶部、导入语句之上添加 "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" 用于声明 Server 和 Client 模块图(树)之间的边界。
一旦文件被标记为 "use client",其所有导入和子组件都被视为客户端包的一部分。这意味着你不需要为每个面向客户端的组件添加该指令。
减少 JS 包大小
为了减少客户端 JavaScript 包的大小,将 'use client' 添加到特定的交互式组件,而不是将 UI 的大部分标记为 Client Components。
例如,<Layout> 组件主要包含静态元素,如徽标和导航链接,但包含一个交互式搜索栏。<Search /> 是交互式的,需要成为 Client Component,但是布局的其余部分可以保持为 Server Component。
// Client Component
import Search from './search'
// Server Component
import Logo from './logo'
// Layout 默认是 Server Component
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<nav>
<Logo />
<Search />
</nav>
<main>{children}</main>
</>
)
}'use client'
export default function Search() {
// ...
}从 Server 向 Client 组件传递数据
你可以使用 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} />
}'use client'
export default function LikeButton({ likes }: { likes: number }) {
// ...
}或者,你可以使用 use Hook 将数据从 Server Component 流式传输到 Client Component。查看示例。
值得注意的是:传递给 Client Components 的 Props 需要能够被 React 序列化。
交错 Server 和 Client 组件
你可以将 Server Components 作为 prop 传递给 Client Component。这允许你在 Client 组件中视觉上嵌套服务器渲染的 UI。
一个常见的模式是使用 children 在 <ClientComponent> 中创建一个_插槽_。例如,在服务器上获取数据的 <Cart> 组件,位于使用客户端状态切换可见性的 <Modal> 组件内部。
'use client'
export default function Modal({ children }: { children: React.ReactNode }) {
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>
)
}在这种模式中,所有 Server Components 都将提前在服务器上渲染,包括作为 props 的那些。生成的 RSC payload 将包含 Client Components 应在组件树中渲染的位置的引用。
Context providers
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>
}然后,将其导入到 Server Component(例如 layout)中:
import ThemeProvider from './theme-provider'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
)
}你的 Server Component 现在将能够直接渲染你的 provider,并且整个应用程序中的所有其他 Client Components 都将能够使用此 context。
值得注意的是:你应该在树中尽可能深地渲染 providers——注意
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(false)
return (
<div>
<button onClick={() => setIsOpen(true)}>View pictures</button>
{/* 可以工作,因为 Carousel 在 Client Component 中使用 */}
{isOpen && <Carousel />}
</div>
)
}但是,如果你尝试直接在 Server Component 中使用它,你会看到错误。这是因为 Next.js 不知道 <Carousel /> 使用仅客户端功能。
要解决这个问题,你可以将依赖于仅客户端功能的第三方组件包装在你自己的 Client Components 中:
'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>
{/* 可以工作,因为 Carousel 是 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()
}此函数包含一个永远不应暴露给客户端的 API_KEY。
在 Next.js 中,只有以 NEXT_PUBLIC_ 为前缀的环境变量才会包含在客户端包中。如果变量没有前缀,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 导入,以在模块在错误环境中使用时提供更清晰的错误消息。来自 NPM 的这些包的内容不被 Next.js 使用。
Next.js 还为 server-only 和 client-only 提供了自己的类型声明,适用于激活了 noUncheckedSideEffectImports 的 TypeScript 配置。