服务端与客户端组件组合模式
在构建 React 应用时,您需要考虑应用的哪些部分应在服务端或客户端渲染。本文介绍了使用服务端组件和客户端组件时的一些推荐组合模式。
何时使用服务端与客户端组件?
以下是服务端组件和客户端组件不同使用场景的快速参考:
您需要实现什么功能? | 服务端组件 | 客户端组件 |
---|---|---|
获取数据 | ||
直接访问后端资源 | ||
在服务端保存敏感信息(访问令牌、API 密钥等) | ||
将大型依赖保留在服务端 / 减少客户端 JavaScript | ||
添加交互性和事件监听器(onClick() 、onChange() 等) | ||
使用状态和生命周期效果(useState() 、useReducer() 、useEffect() 等) | ||
使用仅限浏览器的 API | ||
使用依赖于状态、效果或浏览器 API 的自定义钩子 | ||
使用 React 类组件 |
服务端组件模式
在选择客户端渲染之前,您可能希望在服务端执行某些操作,例如获取数据或访问数据库和后端服务。
以下是使用服务端组件时的常见模式:
在组件间共享数据
在服务端获取数据时,有时需要在不同组件间共享数据。例如,您的布局和页面可能依赖于相同的数据。
无需使用 React Context(在服务端不可用)或通过 props 传递数据,您可以使用 fetch
或 React 的 cache
函数在需要数据的组件中获取相同数据,而无需担心重复请求。这是因为 React 扩展了 fetch
以自动记忆数据请求,当 fetch
不可用时可以使用 cache
函数。
详细了解 React 中的记忆化机制。
防止服务端代码泄漏到客户端环境
由于 JavaScript 模块可以在服务端和客户端组件之间共享,原本仅应在服务端运行的代码可能会意外进入客户端。
例如以下数据获取函数:
乍看之下,getData
似乎在服务端和客户端都能工作。但实际上,该函数包含 API_KEY
,设计初衷是仅在服务端执行。
由于环境变量 API_KEY
没有 NEXT_PUBLIC_
前缀,它是只能在服务端访问的私有变量。为了防止环境变量泄漏到客户端,Next.js 会用空字符串替换私有环境变量。
因此,尽管可以在客户端导入并执行 getData()
,但它不会按预期工作。虽然将变量公开可以使函数在客户端工作,但您可能不希望将敏感信息暴露给客户端。
为了防止这种意外情况,我们可以使用 server-only
包,当其他开发者意外将这些模块导入客户端组件时,会收到构建时错误。
要使用 server-only
,首先安装该包:
然后将该包导入包含服务端代码的任何模块:
现在,任何导入 getData()
的客户端组件都会收到构建时错误,提示该模块只能在服务端使用。
对应的 client-only
包可用于标记包含仅限客户端代码的模块——例如访问 window
对象的代码。
使用第三方包和提供者
由于服务端组件是 React 的新特性,生态系统中的第三方包和提供者刚开始为使用客户端特性的组件(如 useState
、useEffect
和 createContext
)添加 "use client"
指令。
目前,许多使用客户端特性的 npm
包组件尚未添加该指令。这些第三方组件在客户端组件中可以正常工作,因为它们有 "use client"
指令,但在服务端组件中无法工作。
例如,假设您安装了假设的 acme-carousel
包,其中包含 <Carousel />
组件。该组件使用 useState
,但尚未添加 "use client"
指令。
如果在客户端组件中使用 <Carousel />
,它会按预期工作:
但是,如果尝试直接在服务端组件中使用它,您会看到错误:
这是因为 Next.js 不知道 <Carousel />
使用了客户端特性。
要解决此问题,您可以将依赖客户端特性的第三方组件包装在您自己的客户端组件中:
现在,您可以直接在服务端组件中使用 <Carousel />
:
我们不建议您包装大多数第三方组件,因为您很可能在客户端组件中使用它们。但有一个例外是提供者,因为它们依赖于 React 状态和上下文,并且通常需要在应用的根目录中使用。在下方了解更多关于第三方上下文提供者的信息。
使用上下文提供者
上下文提供者通常渲染在应用根目录附近,以共享全局关注点,如当前主题。由于React 上下文在服务端组件中不受支持,尝试在应用根目录创建上下文会导致错误:
要解决此问题,请在客户端组件中创建上下文并渲染其提供者:
现在您的服务端组件可以直接渲染提供者,因为它已被标记为客户端组件:
在根目录渲染提供者后,应用中所有其他客户端组件都可以使用此上下文。
须知:您应尽可能在组件树深处渲染提供者——注意
ThemeProvider
仅包裹{children}
而不是整个<html>
文档。这使得 Next.js 更容易优化服务端组件的静态部分。
给库作者的建议
同样,为其他开发者创建可消费包库的作者可以使用 "use client"
指令标记包的客户端入口点。这使得包的用户可以直接将包组件导入其服务端组件,而无需创建包装边界。
您可以通过在组件树深处使用 'use client' 来优化您的包,使导入的模块成为服务端组件模块图的一部分。
值得注意的是,某些打包工具可能会删除 "use client"
指令。您可以在 React Wrap Balancer 和 Vercel Analytics 仓库中找到如何配置 esbuild 以包含 "use client"
指令的示例。
客户端组件
将客户端组件移至组件树深处
为了减少客户端 JavaScript 包大小,我们建议将客户端组件移至组件树深处。
例如,您可能有一个包含静态元素(如徽标、链接等)和使用了状态的交互式搜索栏的布局。
无需将整个布局设为客户端组件,将交互逻辑移至客户端组件(如 <SearchBar />
),而将布局保留为服务端组件。这意味着您无需将布局的所有组件 JavaScript 发送到客户端。
从服务端组件向客户端组件传递 props(序列化)
如果在服务端组件中获取数据,您可能希望将数据作为 props 传递给客户端组件。从服务端传递给客户端组件的 props 需要能被 React 序列化。
如果您的客户端组件依赖不可序列化的数据,您可以通过第三方库在客户端获取数据,或通过路由处理器在服务端获取。
服务端组件与客户端组件交错使用
当交错使用服务端组件 (Server Components) 和客户端组件 (Client Components) 时,将您的 UI 可视化为组件树会很有帮助。从作为服务端组件的 根布局 (root layout) 开始,您可以通过添加 "use client"
指令在客户端渲染特定的组件子树。
在这些客户端子树中,您仍然可以嵌套服务端组件或调用服务端操作 (Server Actions),但需要注意以下几点:
- 在请求-响应生命周期中,您的代码会从服务端移动到客户端。如果您需要在客户端访问服务端的数据或资源,您将向服务端发起新的请求——而不是来回切换。
- 当向服务端发起新请求时,所有服务端组件会首先渲染,包括嵌套在客户端组件内部的那些。渲染结果(RSC 有效负载)将包含对客户端组件位置的引用。然后,在客户端上,React 会使用 RSC 有效负载将服务端组件和客户端组件协调成单一树结构。
- 由于客户端组件是在服务端组件之后渲染的,您不能将服务端组件导入到客户端组件模块中(因为这会需要向服务端发起新的请求)。相反,您可以将服务端组件作为
props
传递给客户端组件。请参阅下方的不推荐模式和推荐模式部分。
不推荐模式:将服务端组件导入客户端组件
以下模式不被支持。您不能将服务端组件导入到客户端组件中:
推荐模式:将服务端组件作为 Props 传递给客户端组件
以下模式是被支持的。您可以将服务端组件作为 prop 传递给客户端组件。
一种常见模式是使用 React 的 children
prop 在客户端组件中创建一个“插槽”。
在下面的示例中,<ClientComponent>
接受一个 children
prop:
<ClientComponent>
并不知道 children
最终会被服务端组件的结果填充。<ClientComponent>
的唯一职责是决定 children
最终被放置的位置。
在父级服务端组件中,您可以同时导入 <ClientComponent>
和 <ServerComponent>
,并将 <ServerComponent>
作为 <ClientComponent>
的子组件传递:
通过这种方式,<ClientComponent>
和 <ServerComponent>
是解耦的,可以独立渲染。在这种情况下,子组件 <ServerComponent>
可以在服务端渲染,远早于 <ClientComponent>
在客户端渲染。
须知:
- "提升内容 (lifting content up)" 模式已被用于避免在父组件重新渲染时重新渲染嵌套的子组件。
- 您不仅限于使用
children
prop。可以使用任何 prop 来传递 JSX。