跳到主要内容

服务器和客户端组合模式

在构建React应用程序时,您需要考虑应该在服务器端还是客户端端渲染应用程序的哪些部分。 本页面涵盖了在使用服务器组件和客户端组件时的一些推荐组合模式

何时使用服务器组件和客户端组件?

以下是服务器组件和客户端组件的不同用例的快速摘要:

要执行的操作是什么?服务器组件客户端组件
获取数据
访问后端资源(直接)
在服务器上保存敏感信息(访问令牌、API密钥等)
将大型依赖项保留在服务器上/减少客户端JavaScript
添加互动性和事件侦听器(onClick()、onChange()等)
使用状态和生命周期效果(useState()、useReducer()、useEffect()等)
使用仅浏览器API
使用依赖于状态、效果或仅浏览器API的自定义挂钩
使用React类组件

服务器组件模式

在选择客户端渲染之前,您可能希望在服务器上执行一些工作,比如获取数据或访问数据库或后端服务。

在处理服务器组件时,以下是一些常见的模式:

在组件之间共享数据

在服务器上获取数据时,可能存在需要在不同组件之间共享数据的情况。 例如,您可能有一个布局和一个依赖于相同数据的页面。

您可以使用fetch或React的cache函数,在组件中获取相同的数据,而无需担心为相同数据发出重复请求, 而不是使用React Context (在服务器上不可用)或将数据传递为props。 这是因为React会扩展fetch以自动记忆数据请求,当fetch不可用时,可以使用cache函数。

了解有关React中记忆 的更多信息。

将仅在服务器上运行的代码排除在客户端环境之外

由于JavaScript模块可以在服务器和客户端组件模块之间共享,因此仅打算在服务器上运行的代码可能会不经意地进入客户端。

例如,考虑以下数据获取函数:

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()
}

乍一看,看起来getData在服务器和客户端上都可以工作。但是,此函数包含一个API_KEY,旨在仅在服务器上执行。

由于环境变量API_KEY未以NEXT_PUBLIC为前缀,它是一个只能在服务器上访问的私有变量。 为防止环境变量泄漏到客户端,Next.js将私有环境变量替换为空字符串。

因此,即使可以在客户端导入并执行getData(),它也不会按预期工作。 而使变量公开将使函数在客户端上正常工作,但您可能不希望将敏感信息暴露给客户端。

为防止这种意外使用服务器代码的情况,我们可以使用server-only包, 以便在其他开发人员意外导入其中一个这些模块到客户端组件时给予构建时错误。

要使用server-only,首先安装该软件包:

npm install server-only

然后将该软件包导入到包含服务器端代码的任何模块中:

lib/data.js
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()
}

现在,导入getData()的任何客户端组件都将收到一个构建时错误,说明此模块只能在服务器上使用。

相应的client-only软件包可用于标记包含仅客户端代码的模块,例如访问window对象的代码。

使用第三方软件包和提供者

由于服务器组件是React的新功能, 生态系统中的第三方软件包和提供者刚刚开始将"use client"指令添加到使用仅客户端功能 (如useStateuseEffectcreateContext)的组件中。

今天,许多来自npm软件包的组件,这些组件使用仅客户端功能,尚未具有该指令。 这些第三方组件在Client Components中使用时会按预期工作, 因为它们具有"use client"指令,但在Server Components中不起作用。

例如,假设您已安装了假设的acme-carousel软件包,该软件包具有<Carousel />组件。 该组件使用useState,但尚未具有"use client"指令。

如果在Client Component中使用<Carousel />,它将按预期工作:

app/gallery.tsx
'use client'

import { useState } from 'react'
import { Carousel } from 'acme-carousel'

export default function Gallery() {
let [isOpen, setIsOpen] = useState(false)

return (
<div>
<button onClick={() => setIsOpen(true)}>View pictures</button>

{/* 有效,因为Carousel在Client Component中使用 */}
{isOpen && <Carousel />}
</div>
)
}

然而,如果尝试直接在Server Component中使用它,您将看到一个错误:

app/page.tsx
import { Carousel } from 'acme-carousel'

export default function Page() {
return (
<div>
<p>View pictures</p>

{/* 错误:`useState`不能在Server Components中使用 */}
<Carousel />
</div>
)
}

这是因为Next.js不知道<Carousel />正在使用仅客户端功能。

为解决此问题,您可以在自己的Client Components中包装依赖于客户端功能的第三方组件:

app/carousel.tsx
'use client'

import { Carousel } from 'acme-carousel'

export default Carousel

现在,您可以直接在Server Component中使用<Carousel />

app/page.tsx
import Carousel from './carousel'

export default function Page() {
return (
<div>
<p>View pictures</p>

{/* 有效,因为Carousel是Client Component */}
<Carousel />
</div>
)
}

我们不希望您需要包装大多数第三方组件,因为您可能会在Client Components中使用它们。 但是,有一个例外是提供者,因为它们依赖于React状态和上下文,并且通常需要在应用程序的根部。

有关以下的第三方上下文提供程序的更多信息

使用上下文提供程序

上下文提供程序通常在应用程序的根部附近呈现,以共享全局关注点,如当前主题。 由于React context 不受服务器组件支持,因此在应用程序的根部创建上下文将导致错误:

app/layout.tsx
import { createContext } from 'react'

// createContext在服务器组件中不受支持
export const ThemeContext = createContext({})

export default function RootLayout({ children }) {
return (
<html>
<body>
<ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
</body>
</html>
)
}

为解决此问题,请创建您的上下文并在Client Component内部呈现其提供者:

app/theme-provider.tsx
'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将能够直接渲染您的提供者,因为它已被标记为Client Component:

app/layout.tsx
import ThemeProvider from './theme-provider'

export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
)
}

在根部呈现提供程序后,您应用程序中的所有其他Client Component都将能够使用此上下文。

备注

您应该尽可能在树中的深层次呈现提供程序- 请注意ThemeProvider仅包装了{children}而不是整个<html>文档。 这使得Next.js更容易优化服务器组件的静态部分。

库作者的建议

以类似的方式,创建用于被其他开发人员消耗的包的库作者可以使用"use client"指令标记其包的客户端入口点。 这允许包的用户直接将包组件导入其Server Components,而无需创建包装边界。

您可以通过在树的深处使用'use client'来优化包, 允许导入的模块成为Server Component模块图的一部分。

值得注意的是,一些捆绑工具可能会剥离"use client"指令。 您可以在React Wrap BalancerVercel Analytics 存储库中找到如何配置esbuild以在捆绑时包含"use client"指令的示例。

客户端组件

将客户端组件下移树

为了减小客户端JavaScript捆绑包的大小,我们建议将客户端组件下移组件树。

例如,您可能有一个包含静态元素(例如标志、链接等)和使用状态的交互式搜索栏的布局。

与其将整个布局作为客户端组件,不如将交互逻辑移到客户端组件(例如<SearchBar />),并将布局保留为服务器组件。 这意味着您不必将整个布局的组件JavaScript发送到客户端。

app/layout.tsx
// SearchBar是客户端组件
import SearchBar from './searchbar'
// Logo是服务器组件
import Logo from './logo'

// 默认情况下,布局是服务器组件
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<nav>
<Logo />
<SearchBar />
</nav>
<main>{children}</main>
</>
)
}

从服务器传递给客户端组件的props(序列化)

如果在服务器组件中获取数据,您可能希望将数据作为props传递给客户端组件。 从服务器传递给客户端组件的props需要由React进行 序列化

如果您的客户端组件依赖于不可序列化的数据, 您可以使用第三方库在客户端获取数据, 或者通过Route Handler在服务器上获取数据。

交错服务器和客户端组件

在交错客户端和服务器组件时,将UI视为组件树可能会很有帮助。 从根布局 开始,它是一个服务器组件,然后通过添加"use client"指令在客户端上呈现某些子树组件。

在这些客户端子树内,您仍然可以嵌套服务器组件或调用服务器操作,但需要记住一些事项:

  • 在请求-响应生命周期期间,您的代码从服务器移动到客户端。 如果在客户端上需要访问服务器上的数据或资源,您将发起对服务器的新请求-而不是在服务器和客户端之间切换。
  • 当向服务器发出新请求时,首先渲染所有服务器组件,包括嵌套在客户端组件内部的组件。 渲染的结果(RSC Payload)将包含对客户端组件位置的引用。 然后,在客户端上,React使用RSC Payload将服务器和客户端组件协调成一个单一的树。
  • 由于客户端组件在服务器组件之后渲染,因此您不能将服务器组件导入客户端组件模块中(因为这将需要向服务器发起新请求)。 相反,您可以将服务器组件作为props传递给客户端组件。请参见下面的 不受支持的模式受支持的模式部分。

不受支持的模式:将服务器组件导入到客户端组件中

不支持以下模式。您不能将服务器组件导入到客户端组件中:

app/client-component.tsx
'use client'

// 不能将服务器组件导入到客户端组件中。
import ServerComponent from './Server-Component'

export default function ClientComponent({
children,
}: {
children: React.ReactNode
}) {
const [count, setCount] = useState(0)

return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>

<ServerComponent />
</>
)
}

支持的模式:将服务器组件作为props传递给客户端组件

支持以下模式。您可以将服务器组件作为props传递给客户端组件。

一种常见的模式是使用React children prop在客户端组件中创建一个“插槽”。

在下面的示例中,<ClientComponent>接受一个children prop:

app/client-component.tsx
'use client'

import { useState } from 'react'

export default function ClientComponent({
children,
}: {
children: React.ReactNode
}) {
const [count, setCount] = useState(0)

return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
{children}
</>
)
}

<ClientComponent>不知道children最终将由服务器组件的结果填充。 <ClientComponent>唯一的责任是决定children最终将放在何处。

在父服务器组件中, 您可以导入<ClientComponent><ServerComponent>, 并将<ServerComponent>作为<ClientComponent>的子组件传递:

app/page.tsx
// 此模式有效:
// 您可以将服务器组件作为客户端组件的子组件或prop。
import ClientComponent from './client-component'
import ServerComponent from './server-component'

// 在Next.js中,默认情况下,页面是服务器组件
export default function Page() {
return (
<ClientComponent>
<ServerComponent />
</ClientComponent>
)
}

使用这种方法,<ClientComponent><ServerComponent>是解耦的,可以独立呈现。 在这种情况下,子<ServerComponent>可以在服务器上渲染,远早于<ClientComponent>在客户端上渲染。

备注
  • 将内容提升模式已用于在父组件重新渲染时避免重新渲染嵌套的子组件。
  • 您不限于使用children prop。您可以使用任何prop传递JSX。