伺服器和客戶端組合模式
在建置 React 應用程式時,您需要考量應用程式的哪些部分應該在伺服器或客戶端上算繪。此頁面涵蓋使用伺服器和客戶端元件時,一些建議的組合模式。
何時使用伺服器和客戶端元件?
以下是伺服器和客戶端元件不同使用案例的快速摘要
您需要做什麼? | 伺服器元件 | 客戶端元件 |
---|---|---|
獲取資料 | ||
存取後端資源(直接) | ||
將敏感資訊保留在伺服器上(存取權杖、API 金鑰等) | ||
將大型依賴項保留在伺服器上 / 減少客戶端 JavaScript | ||
新增互動性和事件監聽器 (onClick() 、onChange() 等) | ||
使用狀態和生命週期效果 (useState() 、useReducer() 、useEffect() 等) | ||
使用僅限瀏覽器的 API | ||
使用依賴於狀態、效果或僅限瀏覽器 API 的自訂 Hook | ||
使用 React 類別元件 |
伺服器元件模式
在選擇客戶端算繪之前,您可能希望在伺服器上執行一些工作,例如獲取資料,或存取您的資料庫或後端服務。
以下是使用伺服器元件時的一些常見模式
在元件之間共享資料
在伺服器上獲取資料時,可能會有需要跨不同元件共享資料的情況。例如,您可能有一個版面配置和一個頁面都依賴於相同的資料。
您可以不使用 React Context (伺服器上不可用)或將資料作為 props 傳遞,而是可以使用 fetch
或 React 的 cache
函式在需要資料的元件中獲取相同的資料,而不用擔心對相同資料發出重複請求。這是因為 React 擴展了 fetch
以自動記憶資料請求,並且在 fetch
不可用時可以使用 cache
函式。
將僅限伺服器的程式碼排除在客戶端環境之外
由於 JavaScript 模組可以在伺服器和客戶端元件模組之間共享,因此原本只打算在伺服器上運行的程式碼有可能偷偷溜進客戶端。
例如,採用以下資料獲取函式
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
然後將套件導入到任何包含僅限伺服器程式碼的模組中
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"
指令新增到使用僅限客戶端功能的元件,例如 useState
、useEffect
和 createContext
。
如今,許多來自 npm
套件的使用僅限客戶端功能的元件,尚未具有該指令。這些第三方元件將在客戶端元件中按預期運作,因為它們具有 "use client"
指令,但它們將無法在伺服器元件中運作。
例如,假設您已安裝假設的 acme-carousel
套件,其中包含一個 <Carousel />
元件。此元件使用 useState
,但尚未具有 "use client"
指令。
如果您在客戶端元件中使用 <Carousel />
,它將按預期運作
'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>
)
}
但是,如果您嘗試直接在伺服器元件中使用它,您會看到錯誤
import { Carousel } from 'acme-carousel'
export default function Page() {
return (
<div>
<p>View pictures</p>
{/* Error: `useState` can not be used within Server Components */}
<Carousel />
</div>
)
}
這是因為 Next.js 不知道 <Carousel />
正在使用僅限客戶端的功能。
為了修正此問題,您可以將依賴於僅限客戶端功能的第三方元件包裝在您自己的客戶端元件中
'use client'
import { Carousel } from 'acme-carousel'
export default Carousel
現在,您可以直接在伺服器元件中使用 <Carousel />
import Carousel from './carousel'
export default function Page() {
return (
<div>
<p>View pictures</p>
{/* Works, since Carousel is a Client Component */}
<Carousel />
</div>
)
}
我們不期望您需要包裝大多數第三方元件,因為您很可能會在客戶端元件中使用它們。但是,一個例外是供應商,因為它們依賴於 React 狀態和 Context,並且通常需要在應用程式的根目錄中使用。在下方進一步了解第三方 Context Providers。
使用 Context Providers
Context Providers 通常在應用程式的根目錄附近算繪,以共享全域關注的事項,例如目前的主題。由於 React Context 在伺服器元件中不受支援,因此嘗試在應用程式的根目錄建立 Context 會導致錯誤
import { createContext } from 'react'
// createContext is not supported in Server Components
export const ThemeContext = createContext({})
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<body>
<ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
</body>
</html>
)
}
為了修正此問題,請建立您的 Context 並在客戶端元件內部算繪其 Provider
'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>
}
您的伺服器元件現在將能夠直接算繪您的 Provider,因為它已被標記為客戶端元件
import ThemeProvider from './theme-provider'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
)
}
透過在根目錄算繪 Provider,您應用程式中所有其他客戶端元件都將能夠使用此 Context。
要知道的好事:您應該盡可能在樹狀結構中更深層地算繪 Provider – 請注意
ThemeProvider
如何僅包裝{children}
而不是整個<html>
文件。這讓 Next.js 更容易最佳化伺服器元件的靜態部分。
給函式庫作者的建議
以類似的方式,建立要由其他開發人員使用的套件的函式庫作者可以使用 "use client"
指令來標記其套件的客戶端進入點。這允許套件的使用者直接將套件元件導入到其伺服器元件中,而無需建立包裝邊界。
您可以透過在樹狀結構中更深層地使用 「use client」 來最佳化您的套件,從而允許導入的模組成為伺服器元件模組圖的一部分。
值得注意的是,某些捆綁器可能會剝離 "use client"
指令。您可以在 React Wrap Balancer 和 Vercel Analytics 儲存庫中找到如何設定 esbuild 以包含 "use client"
指令的範例。
客戶端元件
將客戶端元件向下移動到樹狀結構中
為了減少客戶端 JavaScript 捆綁包大小,我們建議將客戶端元件向下移動到您的元件樹狀結構中。
例如,您可能有一個具有靜態元素(例如標誌、連結等)和使用狀態的互動式搜尋列的版面配置。
您可以不將整個版面配置設為客戶端元件,而是將互動式邏輯移動到客戶端元件(例如 <SearchBar />
),並將您的版面配置保留為伺服器元件。這表示您不必將版面配置的所有元件 JavaScript 傳送到客戶端。
// SearchBar is a Client Component
import SearchBar from './searchbar'
// Logo is a Server Component
import Logo from './logo'
// Layout is a Server Component by default
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<nav>
<Logo />
<SearchBar />
</nav>
<main>{children}</main>
</>
)
}
從伺服器傳遞 props 到客戶端元件(序列化)
如果您在伺服器元件中獲取資料,您可能希望將資料作為 props 向下傳遞到客戶端元件。從伺服器傳遞到客戶端元件的 Props 需要由 React 序列化。
如果您的客戶端元件依賴於不可序列化的資料,您可以在客戶端上使用第三方函式庫獲取資料,或在伺服器上使用路由處理器獲取資料。
交錯使用伺服器和客戶端元件
當交錯使用客戶端和伺服器元件時,將您的 UI 可視化為元件樹狀結構可能會有所幫助。從根版面配置開始,它是伺服器元件,然後您可以透過新增 "use client"
指令在客戶端上算繪元件的某些子樹狀結構。
在這些客戶端子樹狀結構中,您仍然可以巢狀伺服器元件或呼叫伺服器行為,但是有一些事項需要記住
- 在請求-回應生命週期中,您的程式碼從伺服器移動到客戶端。如果您需要在客戶端上存取伺服器上的資料或資源,您將向伺服器發出新的請求 – 而不是來回切換。
- 當向伺服器發出新請求時,首先會算繪所有伺服器元件,包括巢狀在客戶端元件內部的元件。算繪結果(RSC Payload)將包含對客戶端元件位置的參考。然後,在客戶端上,React 使用 RSC Payload 將伺服器和客戶端元件協調到單個樹狀結構中。
- 由於客戶端元件在伺服器元件之後算繪,因此您無法將伺服器元件導入到客戶端元件模組中(因為這將需要返回伺服器的新請求)。相反,您可以將伺服器元件作為
props
傳遞給客戶端元件。請參閱下方的不支援的模式和支援的模式章節。
不支援的模式:將伺服器元件匯入到客戶端元件中
以下模式不受支援。您無法將伺服器元件導入到客戶端元件中
'use client'
// You cannot import a Server Component into a Client Component.
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 傳遞給客戶端元件
以下模式受到支援。您可以將伺服器元件作為 prop 傳遞給客戶端元件。
一個常見的模式是使用 React children
prop 在您的客戶端元件中建立一個「插槽」。
在下面的範例中,<ClientComponent>
接受一個 children
prop
'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>
的子項傳遞
// This pattern works:
// You can pass a Server Component as a child or prop of a
// Client Component.
import ClientComponent from './client-component'
import ServerComponent from './server-component'
// Pages in Next.js are Server Components by default
export default function Page() {
return (
<ClientComponent>
<ServerComponent />
</ClientComponent>
)
}
使用此方法,<ClientComponent>
和 <ServerComponent>
是解耦的,可以獨立算繪。在這種情況下,子項 <ServerComponent>
可以在伺服器上算繪,遠早於 <ClientComponent>
在客戶端上算繪。
要知道的好事
- 「向上提升內容 (lifting content up)」的模式已被用於避免在父組件重新渲染時,重新渲染巢狀子組件。
- 你不僅限於
children
prop。你可以使用任何 prop 來傳遞 JSX。
這有幫助嗎?