服务端和客户端组合模式
在构建 React 应用时,你需要考虑应用的哪些部分应该在服务器上渲染,哪些部分应该在客户端上渲染。本页面介绍了使用服务端和客户端组件时的一些推荐组合模式。
以下是服务端和客户端组件不同用例的快速总结:
你需要做什么? | 服务端组件 | 客户端组件 |
---|
获取数据 | | |
访问后端资源 (直接) | | |
在服务器上保存敏感信息 (访问令牌、API 密钥等) | | |
保持大型依赖项在服务器上 / 减少客户端 JavaScript | | |
添加交互性和事件监听器 ( onClick() 、onChange() 等) | | |
使用状态和生命周期效果 ( useState() 、useReducer() 、useEffect() 等) | | |
使用仅浏览器可用的 API | | |
使用依赖于状态、效果或仅浏览器可用 API 的自定义 hooks | | |
使用 React 类组件 | | |
在选择客户端渲染之前,你可能希望在服务器上执行一些工作,如获取数据或访问数据库或后端服务。
以下是使用服务端组件时的一些常见模式:
在服务器上获取数据时,可能会有需要在不同组件之间共享数据的情况。例如,你可能有一个布局和一个页面依赖于相同的数据。
你可以使用 fetch
或 React 的 cache
函数在需要数据的组件中获取相同的数据,而不必担心对相同数据发出重复请求,而不是使用 React Context (在服务器上不可用) 或将数据作为 props 传递。这是因为 React 扩展了 fetch
以自动记忆数据请求,而 cache
函数可以在 fetch
不可用时使用。
了解更多关于 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 包的大小,我们建议将客户端组件移到组件树的更深处。
例如,你可能有一个布局,其中包含静态元素(如 logo、链接等)和一个使用状态的交互式搜索栏。
不要将整个布局都设为客户端组件,而是将交互逻辑移到一个客户端组件(例如 <SearchBar />
)中,并保持你的布局作为一个服务端组件。这意味着你不必将布局的所有组件 JavaScript 发送到客户端。
如果你在服务端组件中获取数据,你可能想将数据作为 props 传递给客户端组件。从服务端传递到客户端组件的 props 需要被 React 序列化。
如果你的客户端组件依赖于不可序列化的数据,你可以 在客户端使用第三方库获取数据 或在服务端通过 路由处理程序 获取数据。
当交错使用客户端和服务端组件时,将你的 UI 可视化为一个组件树可能会有所帮助。从 根布局 (它是一个服务端组件)开始,你可以通过添加 "use client"
指令来在客户端渲染某些组件子树。
在这些客户端子树中,你仍然可以嵌套服务端组件或调用服务端操作,但是有一些需要记住的事项:
- 在一个请求-响应生命周期中,你的代码从服务器移动到客户端。如果你需要在客户端上访问服务器上的数据或资源,你将发起一个新的请求到服务器 - 而不是来回切换。
- 当向服务器发出新请求时,所有服务端组件会首先被渲染,包括那些嵌套在客户端组件中的。渲染结果 (RSC Payload) 将包含客户端组件位置的引用。然后,在客户端,React 使用 RSC Payload 将服务端和客户端组件协调成一个单一的树。
- 由于客户端组件是在服务端组件之后渲染的,你不能将服务端组件导入到客户端组件模块中 (因为这需要向服务器发起新的请求)。相反,你可以将服务端组件作为
props
传递给客户端组件。请参阅下面的 不支持的模式 和 支持的模式 部分。
以下模式不受支持。你不能将服务端组件导入到客户端组件中:
以下模式是受支持的。你可以将服务端组件作为 prop 传递给客户端组件。
一个常见的模式是使用 React children
prop 在你的客户端组件中创建一个 "槽位"。
在下面的例子中,<ClientComponent>
接受一个 children
prop:
<ClientComponent>
不知道 children
最终将由服务端组件的结果填充。<ClientComponent>
唯一的责任是决定 children
最终将被放置在何处。
在父服务端组件中,你可以导入 <ClientComponent>
和 <ServerComponent>
,并将 <ServerComponent>
作为 <ClientComponent>
的子组件传递:
通过这种方法,<ClientComponent>
和 <ServerComponent>
是解耦的,可以独立渲染。在这种情况下,子 <ServerComponent>
可以在服务器上渲染,远在 <ClientComponent>
在客户端上渲染之前。
值得注意的是:
- "提升内容" 的模式已被用来避免当父组件重新渲染时重新渲染嵌套的子组件。
- 你不局限于
children
prop。你可以使用任何 prop 来传递 JSX。