<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          React Server Component 從理念到原理

          共 9756字,需瀏覽 20分鐘

           ·

          2023-06-22 03:00


          大廠技術  高級前端  精選文章

          點擊上方 全站前端精選,關注公眾號

          回復1,加入高級前段交流

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

          • 仍屬實驗特性

          • 配置比較繁瑣,且局限較多

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

          本文會從以下幾個角度介紹RSC

          1. RSC是用來做啥的?

          2. RSC和其他服務端渲染方案(SSR、SSG)的區(qū)別

          3. RSC的工作原理

          希望讀者讀完本文后對RSC的應用場景有清晰的認識。

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

          什么是RSC

          對于一個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ù)」邏輯鏈路如果放在前端則比較繁瑣,整個鏈路類似如下:

          1. 前端請求并加載React業(yè)務邏輯代碼

          2. 應用執(zhí)行渲染流程

          3. App組件mount,執(zhí)行useEffect,請求后端數(shù)據(jù)

          4. 后端數(shù)據(jù)返回,App組件的子組件消費數(shù)據(jù)

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

          • 「只包含交互相關狀態(tài)」的組件,叫客戶端組件(React Client Component,簡寫RCC

          • 「只從數(shù)據(jù)源獲取數(shù)據(jù)」的組件,叫服務端組件(React Server Component,簡寫RSC

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

          • App組件只包含數(shù)據(jù),顯然屬于SSR

          • App組件的子組件Ctn消費data,如果他內(nèi)部包含交互邏輯,應該屬于RCC

          將上述代碼改寫為:

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

          其中:

          • App組件在后端運行,可以直接從數(shù)據(jù)源(這里是數(shù)據(jù)庫)獲取數(shù)據(jù)

          • Ctn組件在前端運行,消費數(shù)據(jù)

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

          1. 后端從數(shù)據(jù)源獲取數(shù)據(jù),將RSC數(shù)據(jù)返回給前端

          2. 前端請求并加載業(yè)務邏輯代碼(來自步驟0)

          3. 應用執(zhí)行渲染流程(此時App組件已經(jīng)包含數(shù)據(jù))

          4. App組件的子組件消費數(shù)據(jù)

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

          與SSR、SSG的區(qū)別

          同樣涉及到前端框架的后端運行,RSCSSR、SSG有什么區(qū)別呢?

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

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

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

          • 類似于SSG,SSR的輸出產(chǎn)物是HTML,瀏覽器可以直接解析

          • RSC會流式輸出一種「類JSON」的數(shù)據(jù)結(jié)構(gòu),由前端的React相關插件解析

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

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

          同時,由于實現(xiàn)不同,同一個應用中可以同時存在SSGSSR以及RSC。

          RSC的限制

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

          • 帶有.server.js(x)后綴的文件導出的是RSC

          • 帶有.client.js(x)后綴的文件導出的是RCC

          • 沒有帶serverclient后綴的文件導出的是通用組件

          所以,我們上述例子可以導出為2個文件:

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

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

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

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

          // ClientCpn.client.jsx

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

            )
          }

          這是因為,如果一個組件是RCC,他運行的環(huán)境就是前端,那么他的子孫組件的運行環(huán)境也是前端,但RSC是需要在后端運行的。

          那么上述RSCRCC交替出現(xiàn)是如何實現(xiàn)的呢?

          答案是:通過children

          改寫下ClientCpn.client.jsx

          // ClientCpn.client.jsx

          export default function ClientCpn({children}{
            return (
              <div>{children}</div>
            )
          }

          OuterServerCpn.server.jsx中引入ClientCpnServerCpn

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

            )
          }

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

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

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

          RSC協(xié)議詳解

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

          一款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>服務端組件</div>;
          }

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

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

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

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

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

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

          其中:

          • 「標記」代表這行的數(shù)據(jù)類型,比如J代表「組件樹」,M代表「一個RCC的引用」,S代表Suspense

          • id代表這行數(shù)據(jù)對應的id

          • JSON數(shù)據(jù)保存了這行具體的數(shù)據(jù)

          RSC的序列化與反序列化其實就是JSON的序列化與反序列化。反序列化后的數(shù)據(jù)再根據(jù)「標記」不同做不同處理。

          比如,對于上述代碼中第二行數(shù)據(jù):

          J0:["$","div",null,{"className":"main","children":["$","@1",null,{"children":["$","div",null,{"children":"服務端組件"}]}]}]

          可以理解為,這行數(shù)據(jù)描述了一棵組件樹(標記J),id0,組件樹對應數(shù)據(jù)為:

          [
            "$","div",null,{
              "className":"main","children":[
                "$","@1",null,{
                  "children":["$","div",null,{
                    "children":"服務端組件"}]
                  }
                ]
              }
          ]

          當前端反序列化這行數(shù)據(jù)后,會根據(jù)上述JSON數(shù)據(jù)渲染組件樹。

          id映射

          所謂「id映射」,是指 對于同一個數(shù)據(jù),如何在rpc協(xié)議傳輸?shù)膬啥藢希?/p>

          「RSC協(xié)議」的語境下,是指 對于同一個組件,經(jīng)由RSCReact前后端運行時之間傳遞,是如何對應上的。

          還是考慮上面的例子,回顧下第二行RSC對應的數(shù)據(jù):

          [
            "$","div",null,{
              "className":"main","children":[
                "$","@1",null,{
                  "children":["$","div",null,{
                    "children":"服務端組件"}]
                  }
                ]
              }
          ]

          這段數(shù)據(jù)結(jié)構(gòu)有些類似JSX的返回值,把他與組件層級放到一張圖里對比下:

          可以發(fā)現(xiàn),這些信息已經(jīng)足夠前端渲染<OuterServerCpn/>、<ServerCpn/>組件了,但是<ClientCpn/>對應的數(shù)據(jù)@1是什么意思呢?

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

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

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

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

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

          于是React前端運行時會向這個路徑發(fā)起JSONP請求,請求回<ClientCpn/>組件對應代碼:

          如果應用包裹了<Suspense/>,那么請求過程中會顯示fallback效果。

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

          • M[id],定義id對應的「RCC數(shù)據(jù)」

          • @[id],引用id對應的「RCC數(shù)據(jù)」

          就能將同一個RCCReact前后端運行時對應上。

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

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

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

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

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

          1. 業(yè)務開發(fā)時通過.server | client后綴區(qū)分組件類型

          2. 后端代碼編譯時,所有RCC(即.client后綴文件)會編譯出獨立文件(這一步是react-server-dom-webpack[3]插件做的,對于Vite,也有人提了Vite插件的實現(xiàn) PR[4]

          3. React后端返回給前端的RSC數(shù)據(jù)中包含了組件樹(J標記)等按行表示的數(shù)據(jù)

          4. React前端根據(jù)J標記對應數(shù)據(jù)渲染組件樹,遇到「引用RCC」(形如M[id])時,根據(jù)id發(fā)起JSONP請求

          5. 請求返回該RCC對應組件代碼,請求過程的pending狀態(tài)由<Suspense/>展示

          傳輸協(xié)議

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

          不同于一些rpc協(xié)議會基于TCPUDP實現(xiàn),「RSC協(xié)議」直接基于「HTTP協(xié)議」實現(xiàn),其Content-Typetext/x-component。

          總結(jié)

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

          QRSC和其他服務端渲染方案有什么區(qū)別?

          ARSC是服務端運行時的方案,采用流式傳輸。

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

          A:因為RSC需要在后端獲取數(shù)據(jù)后流式傳輸給前端,而RCC在后端編譯時編譯成獨立文件,前端渲染時再以JSONP的形式請求該文件

          Q:為什么RCC中不能import RSC

          A:因為他們的運行環(huán)境不同(前者在前端,后者在后端)

          由于配置繁瑣,并不推薦在現(xiàn)有React項目中使用RSC。想體驗RSC的同學,可以使用Next.js并開啟App Router

          在這種情況下,組件默認為RSC。

          參考資料

          [1]

          體驗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插件的實現(xiàn) PR: https://github.com/facebook/react/pull/26926

          • 前端 社群



            下方加 Nealyang 好友回復「 加群」即可。



            如果你覺得這篇內(nèi)容對你有幫助,我想請你幫我2個小忙:

            1. 點個「在看」,讓更多人也能看到這篇文章

          瀏覽 19
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  啊使劲用力操骚逼啊视频 | 中国又粗又大性视频 | 大鸡巴具乐部 | 国产内射免费看 | 久久天堂精品 |