前言#
我們知道,在 React Server Component 環境下,渲染的運行時永遠都是在 server 的,而在 RCC 中,兩者環境都可能存在。為了控制一個庫的引用方只能處於某種環境中,而在另一個環境中報錯,我們可以使用 client-only
或者 server-only
庫。
Next.js 是最先支持 React Server Component 的,並且遵循了 server-module-conventions rfc 下文都以 Next.js 展開。
這個 rfc 中指出,在 package.json
中 exports
字段新增了 react-server
導出,這個字段的導出位置只會被 RSC 中的引用鏈使用。
例如,client-only/package.json
是這樣定義的。
{
"exports": {
".": {
"react-server": "./error.js",
"default": "./index.js"
}
}
}
react-server
會寫在第一行,優先被識別。那么在 RSC 組件中引用 client-only
的話實際引用的就是 ./error.js
,這個時候就會報錯。
實際使用場景#
在全新的 React 架構中,RSC + RCC 兩種環境的結合已經是常態了。那么對於庫來說,需要同時兼容兩種環境下的使用方式就需要這個特徵了。
這裡我們以 next-intl 為例,這是一個 i18n 的庫。這個庫的使用方式在 RSC 中還是在 RCC 中都是相同的。例如
在 RCC 中,我們這樣去使用 useTranslations
。
'use client'
import { useTranslations } from "next-intl";
export default function Page() {
const t = useTranslations();
return // ReactNode
}
在 RSC 中,我們這樣去使用 useTranslations
。
import { useTranslations } from "next-intl";
export default async function Page({ params: { locale } }: PageParams) {
unstable_setRequestLocale(locale);
const t = useTranslations();
return // ReactNode
}
你可能發現了,這兩種環境下,使用的方法是一樣的,方法的導出也是一樣的。但是你注意到了,在 RSC 中是不能用 hooks 的,但是這裡卻沒有問題。
這裡就設計到了前面提到的 server-module-conventions 了,在 RSC 中導入的 useTranslations
其實並不是一個 hook 而只是普通的方法,只不過為了保證方法調用的一致性,名稱也是保證了一致。
繼續挖掘 next-intl
的 package.json
發現他的 exports
是這樣定義的。
{
"exports": {
".": {
"types": "./dist/types/src/index.react-client.d.ts",
"react-server": "./dist/esm/index.react-server.js",
"default": "./dist/index.react-client.js"
}
}
}
而在 RSC 中真正指向的其實是 https://github.com/amannn/next-intl/blob/main/packages/next-intl/src/react-server/useTranslations.tsx ,這些都是對 RSC 下的實現。
在業務中使用場景#
這裡列舉一個最近遇到的場景,關於需要在兩個場景下區分 ofetch 實例。我們知道,在 Next.js 的 RCC 和 RSC 下獲取 Cookie 的方式是不一樣的,另外在預渲染頁面時請求的接口總是在 RSC 下發出的,我們或許需要在請求發出時,附加一些請求信息,比如鑒權相關的 header、user-agent、或者轉發真實請求者的 IP 信息等等。
這種情況下,我們可以針對兩個環境編寫兩個不同的實例。
首先,建立一個內部包。例如 packages/fetch
。
建立 package.json
。
{
"name": "@shiro/fetch",
"exports": {
".": {
"react-server": "./src/fetch.server.ts",
"default": "./src/fetch.client.ts"
}
},
"devDependencies": {}
}
編寫 fetch.server.ts
用於 RSC。
import 'server-only'
import { nanoid } from 'nanoid'
import { cookies, headers as nextHeaders } from 'next/headers'
export const $fetch = createFetch({
defaults: {
timeout: 8000,
onRequest(context) {
const cookie = cookies() // 使用 Next.js cookies() 取得 cookie
const token = cookie.get(TokenKey)?.value
const headers: any = context.options.headers ?? {}
if (token) {
headers['Authorization'] = `bearer ${token}`
}
context.options.params ??= {}
if (token) {
context.options.params.r = nanoid()
}
if (context.options.params.token || token) {
context.options.cache = 'no-store' // 命中鑒權後不要告訴 Next.js 不緩存這個請求,反正數據外泄
}
if (isDev) {
console.info(`[Request/Server]: ${context.request}`)
}
const { get } = nextHeaders() // 使用 headers() 方法獲取原始請求頭
const ua = get('user-agent')
const ip =
get('x-real-ip') ||
get('x-forwarded-for') ||
get('remote-addr') ||
get('cf-connecting-ip')
if (ip) {
headers['x-real-ip'] = ip
headers['x-forwarded-for'] = ip
}
headers['User-Agent'] =
`${ua} NextJS/v${PKG.dependencies.next} ${PKG.name}/${PKG.version}`
context.options.headers = headers
},
onResponse(context) {
if (isDev) { // 這裡一定是 ServerSide
console.info(
`[Response/Server]: ${context.request}`,
context.response.status,
)
}
},
},
})
編寫 fetch.client.ts
用於 RCC。
import 'client-only'
import Cookies from 'js-cookie'
function getToken(): string | null { // 這裡只能使用 js-cookie 去獲取瀏覽器端 cookie
const token = Cookies.get(TokenKey)
return token || null
}
export const $fetch = createFetch({
defaults: {
timeout: 8000,
onRequest(context) {
const token = getToken()
const headers: any = context.options.headers ?? {}
if (token) {
headers['Authorization'] = `bearer ${token}`
}
headers['x-session-uuid'] =
globalThis?.sessionStorage?.getItem(uuidStorageKey) ?? uuid
context.options.params ??= {}
if (context.options.params.token) {
context.options.cache = 'no-store'
}
if (isDev && isServerSide) {
// eslint-disable-next-line no-console
console.info(`[Request]: ${context.request}`)
}
context.options.headers = headers
},
onResponse(context) {
if (isDev && isServerSide) { // 這裡還是有必要區分 ServerSide
console.info(`[Response]: ${context.request}`, context.response.status)
}
},
},
})
現在,兩者都有了相同的方法,並且都導出了。那么我們就可以在業務中使用了。為了更好的 TypeScript 支持,我們需要修改 "moduleResolution": "Bundler"
。對了,別忘記,在 package.json
中 link 這個依賴。例如這裡,"@shiro/fetch": "link:./packages/fetch"
添加到項目的 package.json
的 dependencies
中。
使用為:
import { $fetch } from '@shiro/fetch' // 在 RCC 中 RSC 中使用方式一致
上面的實現,已經在 Shiroi 中實現,後續計劃開源到 Shiro 中。
最近被裁了,心情非常鬱悶,比起看八股和刷算法,讓我大腦一片空白,還是研究研究這些才是覺得有意義的。🥹😮💨,只能說選擇創業公司還是謹慎吧。
此文由 Mix Space 同步更新至 xLog 原始鏈接為 https://innei.in/posts/tech/exploring-environment-isolation-and-practice-of-react-server-component-and-react-client-component