掃二維碼與項(xiàng)目經(jīng)理溝通
我們在微信上24小時期待你的聲音
解答本文疑問/技術(shù)咨詢/運(yùn)營咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流
大家好,我卡頌。

React Server Component(后文簡稱RSC)是React近幾年最重要的特性。雖然他對React未來發(fā)展至關(guān)重要,但由于:
所以雖然體驗(yàn)Demo[1]已經(jīng)發(fā)布3年了,但仍屬于「知道的人多,用過的人少」。
本文會從以下幾個角度介紹RSC:
希望讀者讀完本文后對RSC的應(yīng)用場景有清晰的認(rèn)識。
本文參考了how-react-server-components-work[2]
對于一個React組件,可能包含兩種類型的狀態(tài):
function App() {
const [data, update] = useState(null);
useEffect(() => {
fetch(url).then(res => update(res.json()))
}, [])
return ;
} 「前端交互用的狀態(tài)」放在前端很合適,但「后端請求回的數(shù)據(jù)」邏輯鏈路如果放在前端則比較繁瑣,整個鏈路類似如下:
如果我們根據(jù)「狀態(tài)類型」將組件分類,比如:
按照這種邏輯劃分,上述代碼中:
將上述代碼改寫為:
function App() {
// 從數(shù)據(jù)庫獲取數(shù)據(jù)
const data = getDataFromDB();
return ;
} 其中:
改造后「前端交互用的狀態(tài)」邏輯鏈路不變,而「后端請求回的數(shù)據(jù)」邏輯鏈路卻變短很多:
這就是RSC的理念,一句話概括就是 —— 根據(jù)狀態(tài)類型,劃分組件類型,RCC在前端運(yùn)行,RSC在后端運(yùn)行。
同樣涉及到前端框架的后端運(yùn)行,RSC與SSR、SSG有什么區(qū)別呢?
首先,SSG是后端「編譯時方案」。使用SSG的業(yè)務(wù),后端代碼在編譯時會生成HTML(通常會被上傳CDN)。當(dāng)前端發(fā)起請求后,后端(或CDN)始終會返回編譯生成的HTML。
RSC與SSR則都是后端「運(yùn)行時方案」。也就是說,他們都是前端發(fā)起請求后,后端對請求的實(shí)時響應(yīng)。根據(jù)請求參數(shù)不同,可以作出不同響應(yīng)。
同為后端運(yùn)行時方案,RSC與SSR的區(qū)別主要體現(xiàn)在輸出產(chǎn)物:
既然輸出產(chǎn)物不同,那么他們的應(yīng)用場景也是不同的。
比如,在需要考慮SEO(即需要后端直接輸出HTML)時,SSR與SSG可以勝任(都是輸出HTML),而RSC則不行(流式輸出)。
同時,由于實(shí)現(xiàn)不同,同一個應(yīng)用中可以同時存在SSG、SSR以及RSC。
「RSC規(guī)范」是如何區(qū)分RSC與RCC的呢?根據(jù)規(guī)范定義:
所以,我們上述例子可以導(dǎo)出為2個文件:
// app.server.jsx
function App() {
// 從數(shù)據(jù)庫獲取數(shù)據(jù)
const data = getDataFromDB();
return ;
}
// ctn.client.jsx
function Ctn({data}) {
// ...省略邏輯
} 對于任意應(yīng)用,按照「RSC規(guī)范」拆分組件后,能得到類似如下的組件樹,其中RSC和RCC可能交替出現(xiàn):
但是需要注意:RCC中是不允許import RSC的。也就是說,如下寫法是不支持的:
// ClientCpn.client.jsx
import ServerCpn from './ServerCpn.server'
export default function ClientCpn() {
return (
)
}這是因?yàn)?,如果一個組件是RCC,他運(yùn)行的環(huán)境就是前端,那么他的子孫組件的運(yùn)行環(huán)境也是前端,但RSC是需要在后端運(yùn)行的。
那么上述RSC和RCC交替出現(xiàn)是如何實(shí)現(xiàn)的呢?
答案是:通過children。
改寫下ClientCpn.client.jsx:
// ClientCpn.client.jsx
export default function ClientCpn({children}) {
return (
{children}
)
}在OuterServerCpn.server.jsx中引入ClientCpn與ServerCpn:
// OuterServerCpn.server.jsx
import ClientCpn from './ClientCpn.client'
import ServerCpn from './ServerCpn.server'
export default function OuterServerCpn() {
return (
)
}組件結(jié)構(gòu)如下:
解釋下這段代碼,首先OuterServerCpn是RSC,則他運(yùn)行的環(huán)境是后端。他引入的ServerCpn組件運(yùn)行環(huán)境也是后端。
ClientCpn組件雖然運(yùn)行環(huán)境在前端,但是等他運(yùn)行時,他拿到的children props是后端已經(jīng)執(zhí)行完邏輯(已經(jīng)獲得數(shù)據(jù))的ServerCpn組件。
我們可以將RSC看作一種rpc(Remote Procedure Call,遠(yuǎn)程過程調(diào)用)協(xié)議的實(shí)現(xiàn)。數(shù)據(jù)傳輸?shù)膬啥朔謩e是「React后端運(yùn)行時」與「React前端運(yùn)行時」。
一款rpc協(xié)議最基本的組成包括三部分:
以上面的OuterServerCpn.server.jsx舉例:
// OuterServerCpn.server.jsx
import ClientCpn from './ClientCpn.client'
import ServerCpn from './ServerCpn.server'
export default function OuterServerCpn() {
return (
)
}
// ClientCpn.client.jsx
export default function({children}) {
return {children};
}
// ServerCpn.server.jsx
export default function() {
return 服務(wù)端組件;
}這段組件代碼轉(zhuǎn)化為RSC數(shù)據(jù)后如下(不用在意數(shù)據(jù)細(xì)節(jié),后文會解釋):
M1:{"id":"./src/ClientCpn.client.js","chunks":["client1"],"name":""}
J0:["$","div",null,{"className":"main","children":["$","@1",null,{"children":["$","div",null,{"children":"服務(wù)端組件"}]}]}]接下來我們從上述三個角度分析這段數(shù)據(jù)結(jié)構(gòu)的含義。
RSC是一種「按行分隔」的數(shù)據(jù)結(jié)構(gòu)(方便按行流式傳輸),每行的格式為:
[標(biāo)記][id]: JSON數(shù)據(jù)其中:
RSC的序列化與反序列化其實(shí)就是JSON的序列化與反序列化。反序列化后的數(shù)據(jù)再根據(jù)「標(biāo)記」不同做不同處理。
比如,對于上述代碼中第二行數(shù)據(jù):
J0:["$","div",null,{"className":"main","children":["$","@1",null,{"children":["$","div",null,{"children":"服務(wù)端組件"}]}]}]可以理解為,這行數(shù)據(jù)描述了一棵組件樹(標(biāo)記J),id為0,組件樹對應(yīng)數(shù)據(jù)為:
[
"$","div",null,{
"className":"main","children":[
"$","@1",null,{
"children":["$","div",null,{
"children":"服務(wù)端組件"}]
}
]
}
]當(dāng)前端反序列化這行數(shù)據(jù)后,會根據(jù)上述JSON數(shù)據(jù)渲染組件樹。
所謂「id映射」,是指 對于同一個數(shù)據(jù),如何在rpc協(xié)議傳輸?shù)膬啥藢?yīng)上?
在「RSC協(xié)議」的語境下,是指 對于同一個組件,經(jīng)由RSC在React前后端運(yùn)行時之間傳遞,是如何對應(yīng)上的。
還是考慮上面的例子,回顧下第二行RSC對應(yīng)的數(shù)據(jù):
[
"$","div",null,{
"className":"main","children":[
"$","@1",null,{
"children":["$","div",null,{
"children":"服務(wù)端組件"}]
}
]
}
]這段數(shù)據(jù)結(jié)構(gòu)有些類似JSX的返回值,把他與組件層級放到一張圖里對比下:
可以發(fā)現(xiàn),這些信息已經(jīng)足夠前端渲染
這需要結(jié)合第一行RSC的數(shù)據(jù)來分析:
M1:{"id":"./src/ClientCpn.client.js","chunks":["client1"],"name":""}M標(biāo)記代表這行數(shù)據(jù)是「一個RCC的引用」,id為1,數(shù)據(jù)為:
{
"id":"./src/ClientCpn.client.js",
"chunks":["client1"],
"name":""
}第二行中的@1就是指「引用id為1的RCC」,根據(jù)第一行RSC提供的信息,React前端運(yùn)行時知道id為1的RCC包含一個名為client1的chunk,路徑為"./src/ClientCpn.client.js"。
于是React前端運(yùn)行時會向這個路徑發(fā)起JSONP請求,請求回
如果應(yīng)用包裹了
可以看到,通過協(xié)議中的:
就能將同一個RCC在React前后端運(yùn)行時對應(yīng)上。
那么,為什么RCC不像RSC一樣直接返回數(shù)據(jù),而是返回引用id呢?
主要是因?yàn)镽CC中可能包含前端交互邏輯,而有些邏輯是不能通過「RSC協(xié)議」序列化的(底層是JSON序列化)。
比如下面的onClick props是一個函數(shù),函數(shù)是不能通過JSON序列化的:
這里我們再梳理下「RSC協(xié)議」中「id映射」的完整過程:
RSC數(shù)據(jù)是以什么格式在前后端間傳遞呢?
不同于一些rpc協(xié)議會基于TCP或UDP實(shí)現(xiàn),「RSC協(xié)議」直接基于「HTTP協(xié)議」實(shí)現(xiàn),其Content-Type為text/x-component。
本文從理念、原理角度講解了RSC,過程中回答了幾個問題。
Q:RSC和其他服務(wù)端渲染方案有什么區(qū)別?
A:RSC是服務(wù)端運(yùn)行時的方案,采用流式傳輸。
Q:為什么需要區(qū)分RSC與RCC(通過文件后綴)?
A:因?yàn)镽SC需要在后端獲取數(shù)據(jù)后流式傳輸給前端,而RCC在后端編譯時編譯成獨(dú)立文件,前端渲染時再以JSONP的形式請求該文件
Q:為什么RCC中不能import RSC?
A:因?yàn)樗麄兊倪\(yùn)行環(huán)境不同(前者在前端,后者在后端)
由于配置繁瑣,并不推薦在現(xiàn)有React項(xiàng)目中使用RSC。想體驗(yàn)RSC的同學(xué),可以使用Next.js并開啟App Router:
在這種情況下,組件默認(rèn)為RSC。
[1]體驗(yàn)Demo:https://github.com/reactjs/server-components-demo
[2]how-react-server-components-work:https://www.plasmic.app/blog/how-react-server-components-work
[3]react-server-dom-webpack:https://www.npmjs.com/package/react-server-dom-webpack
[4]Vite插件的實(shí)現(xiàn) PR:https://github.com/facebook/react/pull/26926

我們在微信上24小時期待你的聲音
解答本文疑問/技術(shù)咨詢/運(yùn)營咨詢/技術(shù)建議/互聯(lián)網(wǎng)交流