跳到內容
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 配置。