跳到内容
App Router入门服务器和客户端组件

服务器与客户端组件

默认情况下,布局和页面是服务器组件,这让你可以从服务器获取数据并渲染 UI 的部分内容,可以选择缓存结果,并将其流式传输到客户端。当你需要交互性或浏览器 API 时,可以使用客户端组件来分层添加功能。

本页面解释了服务器和客户端组件如何在 Next.js 中工作以及何时使用它们,并提供了如何在应用程序中将它们组合在一起的示例。

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

客户端和服务器环境具有不同的功能。服务器和客户端组件允许你根据用例在每个环境中运行逻辑。

当你需要以下内容时,请使用客户端组件

当你需要以下内容时,请使用服务器组件

  • 从数据库或靠近源头的 API 获取数据。
  • 使用 API 密钥、令牌和其他秘密,而无需将其暴露给客户端。
  • 减少发送到浏览器的 JavaScript 代码量。
  • 改进首次内容绘制 (FCP),并逐步将内容流式传输到客户端。

例如,<Page> 组件是一个服务器组件,它获取有关帖子的数据,并将其作为 prop 传递给处理客户端交互的<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 包含:

  • 服务器组件的渲染结果
  • 应渲染客户端组件的位置的占位符以及对其 JavaScript 文件的引用
  • 从服务器组件传递到客户端组件的任何 props

在客户端(首次加载)

然后,在客户端

  1. HTML 用于立即向用户显示路由的快速非交互式预览。
  2. RSC Payload 用于协调客户端和服务器组件树。
  3. JavaScript 用于水合客户端组件并使应用程序具有交互性。

什么是水合?

水合是 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 包大小

为了减小客户端 JavaScript 包的大小,请将 '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>
    </>
  )
}
app/ui/search.tsx
'use client'
 
export default function Search() {
  // ...
}

从服务器向客户端组件传递数据

你可以使用 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)
 
  return <LikeButton likes={post.likes} />
}
app/ui/like-button.tsx
'use client'
 
export default function LikeButton({ likes }: { likes: number }) {
  // ...
}

或者,你可以使用use Hook从服务器组件向客户端组件流式传输数据。请参阅一个示例

须知:传递给客户端组件的 props 需要被 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 payload 将包含客户端组件在组件树中应该渲染位置的引用。

上下文提供程序

React context通常用于共享全局状态,例如当前主题。然而,React context 在服务器组件中不受支持。

要使用 context,请创建一个接受 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>
  )
}

您的服务器组件现在将能够直接渲染您的提供者,并且您应用程序中的所有其他客户端组件都将能够使用此上下文。

须知:您应该尽可能深地渲染提供者——请注意 ThemeProvider 仅包装了 {children} 而不是整个 <html> 文档。这使得 Next.js 更容易优化服务器组件的静态部分。

第三方组件

使用依赖于仅客户端功能的第三方组件时,可以将其包装在客户端组件中,以确保其按预期工作。

例如,<Carousel /> 可以从 acme-carousel 包中导入。此组件使用 useState,但它尚未包含 "use client" 指令。

如果你在客户端组件中使用 <Carousel />,它将按预期工作。

app/gallery.tsx
'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>
      {/* Works, since Carousel is used within a Client Component */}
      {isOpen && <Carousel />}
    </div>
  )
}

但是,如果您尝试直接在服务器组件中使用它,您将看到一个错误。这是因为 Next.js 不知道 <Carousel /> 正在使用仅客户端功能。

为了解决这个问题,你可以将依赖于客户端特有功能的第三方组件包装在自己的客户端组件中。

app/carousel.tsx
'use client'
 
import { Carousel } from 'acme-carousel'
 
export default Carousel

现在,您可以直接在服务器组件中使用 <Carousel />

app/page.tsx
import Carousel from './carousel'
 
export default function Page() {
  return (
    <div>
      <p>View pictures</p>
      {/*  Works, since Carousel is a Client Component */}
      <Carousel />
    </div>
  )
}

给库作者的建议

如果您正在构建组件库,请将 "use client" 指令添加到依赖客户端专属功能的入口点。这允许您的用户将组件导入到服务器组件中,而无需创建包装器。

值得注意的是,某些打包器可能会剥离 "use client" 指令。您可以在 React Wrap BalancerVercel Analytics 仓库中找到如何配置 esbuild 以包含 "use client" 指令的示例。

防止环境污染

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

此函数包含一个不应暴露给客户端的 API_KEY

在 Next.js 中,只有以 NEXT_PUBLIC_ 为前缀的环境变量才包含在客户端 bundle 中。如果变量没有前缀,Next.js 会将其替换为空字符串。

因此,即使 getData() 可以在客户端导入和执行,它也不会按预期工作。

为防止在客户端组件中意外使用,可以使用 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()
}

现在,如果您尝试将该模块导入到客户端组件中,将会出现构建时错误。

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

在 Next.js 中,安装 server-onlyclient-only可选的。但是,如果您的 linting 规则标记了多余的依赖项,您可以安装它们以避免问题。

终端
pnpm add server-only

Next.js 内部处理 server-onlyclient-only 导入,以便在模块在错误环境中使用时提供更清晰的错误消息。Next.js 不使用来自 NPM 的这些包的内容。

Next.js 还为 server-onlyclient-only 提供了自己的类型声明,适用于 noUncheckedSideEffectImports 处于活动状态的 TypeScript 配置。