自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

React Server Component 從理念到原理

開發(fā) 前端
SSG是后端「編譯時(shí)方案」。使用SSG的業(yè)務(wù),后端代碼在編譯時(shí)會(huì)生成HTML(通常會(huì)被上傳CDN)。當(dāng)前端發(fā)起請求后,后端(或CDN)始終會(huì)返回編譯生成的HTML。

大家好,我卡頌。

React Server Component(后文簡稱RSC)是React近幾年最重要的特性。雖然他對React未來發(fā)展至關(guān)重要,但由于:

  • 仍屬實(shí)驗(yàn)特性。
  • 配置比較繁瑣,且局限較多。

所以雖然體驗(yàn)Demo[1]已經(jīng)發(fā)布3年了,但仍屬于「知道的人多,用過的人少」。

本文會(huì)從以下幾個(gè)角度介紹RSC:

  1. RSC是用來做啥的?
  2. RSC和其他服務(wù)端渲染方案(SSR、SSG)的區(qū)別
  3. RSC的工作原理

希望讀者讀完本文后對RSC的應(yīng)用場景有清晰的認(rèn)識(shí)。

本文參考了how-react-server-components-work[2]

什么是RSC

對于一個(gè)React組件,可能包含兩種類型的狀態(tài):

  • 前端交互用的狀態(tài),比如加載按鈕的顯/隱狀態(tài)
  • 后端請求回的數(shù)據(jù),比如下面代碼中的data狀態(tài)用于保存后端數(shù)據(jù):
function App() {
  const [data, update] = useState(null);
  
  useEffect(() => {
    fetch(url).then(res => update(res.json()))
  }, [])
  
  return <Ctn data={data}/>;
}

「前端交互用的狀態(tài)」放在前端很合適,但「后端請求回的數(shù)據(jù)」邏輯鏈路如果放在前端則比較繁瑣,整個(gè)鏈路類似如下:

  1. 前端請求并加載React業(yè)務(wù)邏輯代碼。
  2. 應(yīng)用執(zhí)行渲染流程。
  3. App組件mount,執(zhí)行useEffect,請求后端數(shù)據(jù)。
  4. 后端數(shù)據(jù)返回,App組件的子組件消費(fèi)數(shù)據(jù)。

如果我們根據(jù)「狀態(tài)類型」將組件分類,比如:

  • 「只包含交互相關(guān)狀態(tài)」的組件,叫客戶端組件(React Client Component,簡寫RCC)。
  • 「只從數(shù)據(jù)源獲取數(shù)據(jù)」的組件,叫服務(wù)端組件(React Server Component,簡寫RSC)。

按照這種邏輯劃分,上述代碼中:

  • App組件只包含數(shù)據(jù),顯然屬于SSR。
  • App組件的子組件Ctn消費(fèi)data,如果他內(nèi)部包含交互邏輯,應(yīng)該屬于RCC。

將上述代碼改寫為:

function App() {
  // 從數(shù)據(jù)庫獲取數(shù)據(jù)
  const data = getDataFromDB();
  return <Ctn data={data}/>;
}

其中:

  • App組件在后端運(yùn)行,可以直接從數(shù)據(jù)源(這里是數(shù)據(jù)庫)獲取數(shù)據(jù)
  • Ctn組件在前端運(yùn)行,消費(fèi)數(shù)據(jù)

改造后「前端交互用的狀態(tài)」邏輯鏈路不變,而「后端請求回的數(shù)據(jù)」邏輯鏈路卻變短很多:

  1. 后端從數(shù)據(jù)源獲取數(shù)據(jù),將RSC數(shù)據(jù)返回給前端。
  2. 前端請求并加載業(yè)務(wù)邏輯代碼(來自步驟0)。
  3. 應(yīng)用執(zhí)行渲染流程(此時(shí)App組件已經(jīng)包含數(shù)據(jù))。
  4. App組件的子組件消費(fèi)數(shù)據(jù)。

這就是RSC的理念,一句話概括就是 —— 根據(jù)狀態(tài)類型,劃分組件類型,RCC在前端運(yùn)行,RSC在后端運(yùn)行。

與SSR、SSG的區(qū)別

同樣涉及到前端框架的后端運(yùn)行,RSC與SSR、SSG有什么區(qū)別呢?

首先,SSG是后端「編譯時(shí)方案」。使用SSG的業(yè)務(wù),后端代碼在編譯時(shí)會(huì)生成HTML(通常會(huì)被上傳CDN)。當(dāng)前端發(fā)起請求后,后端(或CDN)始終會(huì)返回編譯生成的HTML。

RSC與SSR則都是后端「運(yùn)行時(shí)方案」。也就是說,他們都是前端發(fā)起請求后,后端對請求的實(shí)時(shí)響應(yīng)。根據(jù)請求參數(shù)不同,可以作出不同響應(yīng)。

同為后端運(yùn)行時(shí)方案,RSC與SSR的區(qū)別主要體現(xiàn)在輸出產(chǎn)物:

  • 類似于SSG,SSR的輸出產(chǎn)物是HTML,瀏覽器可以直接解析。
  • RSC會(huì)流式輸出一種「類JSON」的數(shù)據(jù)結(jié)構(gòu),由前端的React相關(guān)插件解析。

既然輸出產(chǎn)物不同,那么他們的應(yīng)用場景也是不同的。

比如,在需要考慮SEO(即需要后端直接輸出HTML)時(shí),SSR與SSG可以勝任(都是輸出HTML),而RSC則不行(流式輸出)。

同時(shí),由于實(shí)現(xiàn)不同,同一個(gè)應(yīng)用中可以同時(shí)存在SSG、SSR以及RSC。

RSC的限制

「RSC規(guī)范」是如何區(qū)分RSC與RCC的呢?根據(jù)規(guī)范定義:

  • 帶有.server.js(x)后綴的文件導(dǎo)出的是RSC。
  • 帶有.client.js(x)后綴的文件導(dǎo)出的是RCC。
  • 沒有帶server或client后綴的文件導(dǎo)出的是通用組件。

所以,我們上述例子可以導(dǎo)出為2個(gè)文件:

// app.server.jsx
function App() {
  // 從數(shù)據(jù)庫獲取數(shù)據(jù)
  const data = getDataFromDB();
  return <Ctn data={data}/>;
}

// ctn.client.jsx
function Ctn({data}) {
  // ...省略邏輯
}

對于任意應(yīng)用,按照「RSC規(guī)范」拆分組件后,能得到類似如下的組件樹,其中RSCRCC可能交替出現(xiàn):

圖片

但是需要注意:RCC中是不允許import RSC的。也就是說,如下寫法是不支持的:

// ClientCpn.client.jsx

import ServerCpn from './ServerCpn.server'
export default function ClientCpn() {
  return (
    <div>
      <ServerCpn />
    </div>
  )
}

這是因?yàn)?,如果一個(gè)組件是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 (
    <div>{children}</div>
  )
}

在OuterServerCpn.server.jsx中引入ClientCpn與ServerCpn:

// OuterServerCpn.server.jsx
import ClientCpn from './ClientCpn.client'
import ServerCpn from './ServerCpn.server'
export default function OuterServerCpn() {
  return (
    <ClientCpn>
      <ServerCpn />
    </ClientCpn>
  )
}

組件結(jié)構(gòu)如下:

解釋下這段代碼,首先OuterServerCpn是RSC,則他運(yùn)行的環(huán)境是后端。他引入的ServerCpn組件運(yùn)行環(huán)境也是后端。

ClientCpn組件雖然運(yùn)行環(huán)境在前端,但是等他運(yùn)行時(shí),他拿到的children props是后端已經(jīng)執(zhí)行完邏輯(已經(jīng)獲得數(shù)據(jù))的ServerCpn組件。

RSC協(xié)議詳解

我們可以將RSC看作一種rpc(Remote Procedure Call,遠(yuǎn)程過程調(diào)用)協(xié)議的實(shí)現(xiàn)。數(shù)據(jù)傳輸?shù)膬啥朔謩e是「React后端運(yùn)行時(shí)」與「React前端運(yùn)行時(shí)」。

圖片

一款rpc協(xié)議最基本的組成包括三部分:

  • 數(shù)據(jù)的序列化與反序列化
  • id映射
  • 傳輸協(xié)議

以上面的OuterServerCpn.server.jsx舉例:

// OuterServerCpn.server.jsx
import ClientCpn from './ClientCpn.client'
import ServerCpn from './ServerCpn.server'
export default function OuterServerCpn() {
  return (
    <ClientCpn>
      <ServerCpn />
    </ClientCpn>
  )
}

// ClientCpn.client.jsx
export default function({children}) {
  return <div>{children}</div>;
}

// ServerCpn.server.jsx
export default function() {
  return <div>服務(wù)端組件</div>;
}

這段組件代碼轉(zhuǎn)化為RSC數(shù)據(jù)后如下(不用在意數(shù)據(jù)細(xì)節(jié),后文會(huì)解釋):

M1:{"id":"./src/ClientCpn.client.js","chunks":["client1"],"name":""}
J0:["$","div",null,{"className":"main","children":["$","@1",null,{"children":["$","div",null,{"children":"服務(wù)端組件"}]}]}]

接下來我們從上述三個(gè)角度分析這段數(shù)據(jù)結(jié)構(gòu)的含義。

數(shù)據(jù)的序列化與反序列化

RSC是一種「按行分隔」的數(shù)據(jù)結(jié)構(gòu)(方便按行流式傳輸),每行的格式為:

[標(biāo)記][id]: JSON數(shù)據(jù)

其中:

  • 「標(biāo)記」代表這行的數(shù)據(jù)類型,比如J代表「組件樹」,M代表「一個(gè)RCC的引用」,S代表Suspense
  • id代表這行數(shù)據(jù)對應(yīng)的id。
  • JSON數(shù)據(jù)保存了這行具體的數(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ù)后,會(huì)根據(jù)上述JSON數(shù)據(jù)渲染組件樹。

id映射

所謂「id映射」,是指 對于同一個(gè)數(shù)據(jù),如何在rpc協(xié)議傳輸?shù)膬啥藢?yīng)上?

在「RSC協(xié)議」的語境下,是指 對于同一個(gè)組件,經(jīng)由RSC在React前后端運(yùn)行時(shí)之間傳遞,是如何對應(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)足夠前端渲染<OuterServerCpn/>、<ServerCpn/>組件了,但是<ClientCpn/>對應(yīng)的數(shù)據(jù)@1是什么意思呢?

這需要結(jié)合第一行RSC的數(shù)據(jù)來分析:

M1:{"id":"./src/ClientCpn.client.js","chunks":["client1"],"name":""}

M標(biāo)記代表這行數(shù)據(jù)是「一個(gè)RCC的引用」,id為1,數(shù)據(jù)為:

{
  "id":"./src/ClientCpn.client.js",
  "chunks":["client1"],
  "name":""
}

第二行中的@1就是指「引用id為1的RCC」,根據(jù)第一行RSC提供的信息,React前端運(yùn)行時(shí)知道id為1的RCC包含一個(gè)名為client1的chunk,路徑為"./src/ClientCpn.client.js"。

于是React前端運(yùn)行時(shí)會(huì)向這個(gè)路徑發(fā)起JSONP請求,請求回<ClientCpn/>組件對應(yīng)代碼:

如果應(yīng)用包裹了<Suspense/>,那么請求過程中會(huì)顯示fallback效果。

可以看到,通過協(xié)議中的:

  • M[id],定義id對應(yīng)的「RCC數(shù)據(jù)」。
  • @[id],引用id對應(yīng)的「RCC數(shù)據(jù)」。

就能將同一個(gè)RCC在React前后端運(yùn)行時(shí)對應(yīng)上。

那么,為什么RCC不像RSC一樣直接返回?cái)?shù)據(jù),而是返回引用id呢?

主要是因?yàn)镽CC中可能包含前端交互邏輯,而有些邏輯是不能通過「RSC協(xié)議」序列化的(底層是JSON序列化)。

比如下面的onClick props是一個(gè)函數(shù),函數(shù)是不能通過JSON序列化的:

<button onClick={() => console.log('hello')}>你好</button>

這里我們再梳理下「RSC協(xié)議」中「id映射」的完整過程:

  1. 業(yè)務(wù)開發(fā)時(shí)通過.server | client后綴區(qū)分組件類型。
  2. 后端代碼編譯時(shí),所有RCC(即.client后綴文件)會(huì)編譯出獨(dú)立文件(這一步是react-server-dom-webpack[3]插件做的,對于Vite,也有人提了Vite插件的實(shí)現(xiàn) PR[4])。
  3. React后端返回給前端的RSC數(shù)據(jù)中包含了組件樹(J標(biāo)記)等按行表示的數(shù)據(jù)。
  4. React前端根據(jù)J標(biāo)記對應(yīng)數(shù)據(jù)渲染組件樹,遇到「引用RCC」(形如M[id])時(shí),根據(jù)id發(fā)起JSONP請求。
  5. 請求返回該RCC對應(yīng)組件代碼,請求過程的pending狀態(tài)由<Suspense/>展示。

傳輸協(xié)議

RSC數(shù)據(jù)是以什么格式在前后端間傳遞呢?

不同于一些rpc協(xié)議會(huì)基于TCP或UDP實(shí)現(xiàn),「RSC協(xié)議」直接基于「HTTP協(xié)議」實(shí)現(xiàn),其Content-Type為text/x-component。

圖片

總結(jié)

本文從理念、原理角度講解了RSC,過程中回答了幾個(gè)問題。

Q:RSC和其他服務(wù)端渲染方案有什么區(qū)別?

A:RSC是服務(wù)端運(yùn)行時(shí)的方案,采用流式傳輸。

Q:為什么需要區(qū)分RSC與RCC(通過文件后綴)?

A:因?yàn)镽SC需要在后端獲取數(shù)據(jù)后流式傳輸給前端,而RCC在后端編譯時(shí)編譯成獨(dú)立文件,前端渲染時(shí)再以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

責(zé)任編輯:姜華 來源: 魔術(shù)師卡頌
相關(guān)推薦

2014-04-24 11:49:42

DevOps自動(dòng)化

2021-10-12 08:34:23

React組件前端

2009-04-08 16:42:08

動(dòng)態(tài)架構(gòu)DI智慧IT

2022-09-19 19:51:30

ReactuseEffect

2021-05-11 07:51:30

React ref 前端

2021-07-21 15:16:53

云原生阿里云云安全

2023-11-16 07:43:26

Next.jsReact

2016-01-14 09:30:46

Hive概念安裝使用

2024-05-29 12:50:49

2025-04-02 07:29:14

2022-05-25 16:32:36

云原生Cloud

2023-05-26 08:13:49

RSCNext.js開源

2012-04-25 10:02:39

H3CNGIP

2009-07-09 18:20:53

云存儲(chǔ)云計(jì)算云服務(wù)

2010-06-03 14:22:01

2017-07-25 16:04:31

概念應(yīng)用強(qiáng)化學(xué)習(xí)

2010-08-25 17:05:41

DHCP服務(wù)器

2013-08-19 11:55:48

華為HCC大會(huì)HCC2013華為

2014-06-04 13:20:52

大數(shù)據(jù)

2019-04-17 09:53:11

物聯(lián)網(wǎng)網(wǎng)關(guān)物聯(lián)網(wǎng)IOT
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)