Server and Client Components
默认情况下,Layout 和 Page 是服务端组件,它允许您在服务器上获取数据并渲染部分 UI,并可选择缓存结果,然后将其流式传输到客户端。当您需要交互功能或浏览器 API 时,可以使用客户端组件分层功能
本页解释了服务器和客户端组件在 Next.js 中的工作方式以及何时使用它们,并提供了如何在应用程序中将它们组合在一起的示例
何时使用服务端和客户端组件?
客户端和服务端环境具有不同的功能。服务器和客户端组件允许您根据用例在各个环境中运行逻辑
在需要时使用客户端组件:
- State 和事件处理程序(如
onClick
) - 生命周期逻辑(如
useEffect
) - 仅限浏览器的 API(如
localStorage
、window
、Navigator.geolocation
) - 自定义 hooks
在需要时使用服务器组件:
- 从数据库或 API 获取数据
- 使用 API 密钥、令牌和其他机密,但不将它们暴露给客户端
- 减少发送到浏览器的 JS 数量
- 改进首次内容绘制 (FCP),并将内容逐步传输到客户
例如, <Page>
组件是一个服务器组件,它获取有关帖子的数据,并将其作为 props 传递给处理客户端交互的 <LikeButton>
// app/[id]/page.tsx
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>
)
}
// app/ui/like-button.tsx
'use client'
import { useState } from 'react'
export default function LikeButton({ likes }: { likes: number }) {
// ...
}
服务端和客户端组件在 Next.js 中如何工作
在服务端
在服务端上,Next.js 使用 React 的 API 来编排渲染。渲染工作被拆分成多个块,每个块由单独的路由段组成:
- 服务端组件被渲染成一种称为 React 服务端组件载荷(RSC Payload)的特殊数据格式
- 客户端组件和 RSC Payload 用于预渲染HTML
什么是 React 服务端组件载荷 (RSC)?
RSC Payload 是已渲染的 React 服务端组件树的紧凑二进制表示。React 在客户端使用它来更新浏览器的 DOM
RSC Payload 包含:
- 服务端组件渲染结果
- 客户端组件渲染位置的占位符以及对其 JS 文件的引用
- 从服务端组件传递到客户端组件的任何 props
在客户端(首次加载)
然后,在客户端上:
- HTML 用于立即向用户显示对应路由的快速非交互式预览
- RSC Payload 用于协调客户端和服务器组件树
- JavaScript 用于补充客户端组件并使应用程序具有交互性
什么是水合作用 (hydration)?
Hydration 是 React 附加事件处理程序到 DOM 的过程,使静态 HTML 具有交互性
后续导航
在后续导航中:
- RSC Payload 已预取并缓存,以便即时导航
- 客户端组件完全在客户端上呈现,无需服务器呈现的 HTML
示例
使用客户端组件
您可以通过在文件顶部添加 "use client"
指令来创建客户端组件:
// app/ui/counter.tsx
'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"
用于声明服务器和客户端模块图(树)之间的边界
一旦文件被标记为 "use client"
,其所有导入和子组件都将被视为客户端 bundle 的一部分。这意味着您无需将该指令添加到每个用于客户端的组件中
减少 JS 包大小
为了减少客户端 JS 包的大小,请将 'use client'
添加到特定的交互式组件,而不是将 UI 的大部分标记为客户端组件
例如,<Layout>
组件主要包含徽标和导航链接等静态元素,但也包含一个交互式搜索栏。<Search />
是交互式的,需要是客户端组件,但是,其余布局可以保留为服务器组件
// app/layout.tsx
// 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>
</>
)
}
将数据从服务端传递到客户端组件
您可以使用 props 将数据从服务器组件传递到客户端组件
// app/[id]/page.tsx
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)
// 使用 props 将数据从服务器组件传递到客户端组件
return <LikeButton likes={post.likes} />
}
或者,你可以使用 use
Hook 将数据从服务器组件传输到客户端组件
需要了解:传递给客户端组件的 Prop 需要可序列化由 React 提供
交错服务器和客户端组件
您可以将服务器组件作为 prop 传递给客户端组件。这样您就可以在客户端组件中直接地嵌套服务器渲染的 UI
一种常见的模式是使用 children
在 <ClientComponent>
中创建一个插槽。例如,一个 <Cart>
组件用于从服务器获取数据,而 <Modal>
组件则使用客户端状态来切换可见性
// app/ui/modal.tsx
'use client'
export default function Modal({ children }: { children: React.ReactNode }) {
return <div>{children}</div>
}
然后,在父服务器组件(例如 <Page>
)中,您可以将 <Cart>
作为 <Modal>
的子组件传递:
// app/page.tsx
import Modal from './ui/modal'
import Cart from './ui/cart'
export default function Page() {
return (
<Modal>
<Cart />
</Modal>
)
}
在此模式中,所有服务端组件(包括作为 props 的组件)都将提前在服务器上渲染。生成的 RSC 负载将包含客户端组件在组件树中渲染位置的引用
Context providers
React context 通常用于共享全局状态,例如当前主题。但是,服务器组件不支持 React 上下文
要使用上下文,请创建一个接受 children
的客户端组件:
// app/theme-provider.tsx
'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>
}
然后,将其导入到服务器组件(例如 layout
):
// app/layout.tsx
import ThemeProvider from './theme-provider'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
)
}
您的服务端组件现在将能够直接渲染 Provider,并且您应用程序中的所有其他客户端组件都将能够使用此上下文
温馨提示
您应该在树中尽可能深地渲染 Provider——请注意
ThemeProvider
仅包装{children}
而不是整个<html>
文档。这使得 Next.js 更容易优化服务器组件的静态部分
第三方组件
当使用依赖于仅客户端功能的第三方组件时,您可以将其包装在客户端组件中以确保其按预期工作
例如, <Carousel />
可以从 acme-carousel
包中导入。该组件使用了 useState
,但尚未包含 "use client"
指令
如果您在客户端组件中使用 <Carousel />
,它将按预期工作
但是,如果您尝试直接在服务端组件中使用它,则会看到错误。这是因为 Next.js 不知道 <Carousel />
正在使用仅限客户端的功能
为了解决这个问题,您可以将依赖于仅客户端功能的第三方组件包装在您自己的客户端组件中:
// app/carousel.tsx
'use client'
import { Carousel } from 'acme-carousel'
export default Carousel
给库作者的建议
如果您正在构建组件库,请将
"use client"
指令添加到依赖仅客户端功能的入口点。这样,您的用户无需创建包装器即可将组件导入服务端组件值得注意的是,某些打包器可能会删除
"use client"
指令。您可以在 React Wrap Balancer 和 Vercel Analytics 存储库中找到有关如何配置 esbuild 以包含"use client"
指令的示例
预防环境中毒
JS 模块可以在服务端和客户端组件模块之间共享。这意味着可能会意外地将服务端专用代码导入到客户端。例如,考虑以下函数:
// lib/data.ts
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()
,但它不会按预期工作
为了防止在客户端组件中意外使用,您可以使用 server-only
包,然后,将包导入到包含仅服务端代码的文件中:
// lib/data.ts
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-only
包可用于标记包含仅客户端逻辑的模块,例如访问 window
对象的代码
在 Next.js 中,安装 server-only
或 client-only
是可选的 。但是,如果您的 linting 规则标记了无关的依赖项,您可以安装它们以避免出现问题
Next.js 内部处理 server-only
和 client-only
导入,以便在错误的环境中使用模块时提供更清晰的错误消息。Next.js 不会使用这些来自 NPM 的软件包的内容
Next.js 还为 server-only
和 client-only
提供了自己的类型声明,用于 noUncheckedSideEffectImports
TS 告警