<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      React Streaming SSR 原理解析

      作者:徐超

      功能簡介

      React 18 提供了一種新的 SSR 渲染模式: Streaming SSR。通過 Streaming SSR,我們可以實現以下兩個功能:

      • Streaming HTML:服務端可以分段傳輸 HTML 到瀏覽器,而不是像 React 18 以前一樣,需要等待服務端渲染完成整個頁面后才返回給瀏覽器。這樣,瀏覽器可以更快的啟動 HTML 的渲染,提高 FP、FCP 等性能指標。
      • Selective Hydration:在瀏覽器端 hydration 階段,可以只對已經完成渲染的區域做 hydration,而不需要等待整個頁面渲染完成、所有組件的 JS  bundle 加載完成,才能開始 hydration。這樣可以更早的對已經完成渲染的區域做事件綁定,從而讓頁面獲得更好的可交互性。

      基本原理

      使用示例

      React 官網給出的一個簡單的使用示例(以 Node.js 環境下的 API 為例)如下:

      let didError = false;
      const stream = renderToPipeableStream(
        <App />,
        { 
          bootstrapScripts: ["main.js"],
          onShellReady() {
            // The content above all Suspense boundaries is ready.
            // If something errored before we started streaming, 
            // we set the error code appropriately.
            res.statusCode = didError ? 500 : 200;
            res.setHeader('Content-type', 'text/html');
            stream.pipe(res);
          },
          onShellError(error) {
            // Something errored before we could complete the shell 
            // so we emit an alternative shell.
            res.statusCode = 500;
            res.send('<!doctype html><p>Loading...</p><script src="clientrender.js"></script>');
          },
          onAllReady() {
            // stream.pipe(res);
          },
          onError(err) {
            didError = true;
            console.error(err);
          }
        }
      );
      

      renderToPipeableStream 是在 Node.js 環境下實現 Streaming SSR 的 API。

      Streaming HTML

      HTTP 支持以 stream 格式進行數據傳輸。當 HTTP 的 Response header 設置 Transfer-Encoding: chunked 時,服務器端就可以將 Response 分段返回。一個簡單示例:

      const http = require("http");
      const url = require("url");
      
      const sleep = (ms) => {
        return new Promise((resolve) => {
          setTimeout(resolve, ms);
        });
      };
      
      const server = http.createServer(async (req, res) => {
        const { pathname } = url.parse(req.url);
        if (pathname === "/") {
          res.statusCode = 200;
          res.setHeader("Content-Type", "text/html");
          res.setHeader("Transfer-Encoding", "chunked");
          res.write("<html><body><div>First segment</div>");
          // 手動設置延時,讓分段顯示的效果更加明顯
          await sleep(2000);
          res.write("<div>Second segment</div></body></html>");
          res.end();
          return;
        }
      
        res.writeHead(200, { "Content-Type": "text/plain" });
        res.end("okay");
      });
      
      server.listen(8080);
      

      當訪問 localhost:8080 時,「First segment」 和 「Second segment」會分 2 次傳輸到瀏覽器端,「First segment」先顯示到頁面上,2s 延遲后,「Second segment」再顯示到頁面上。

      React 中的 Streaming HTML 要更加復雜。例如,對下面的 App 組件做 SSR:

      //文件1: Content.js
      export default function Content() {
        return (
          <div> This is content </div>
        );
      }
      
      // 文件2:App.js
      import { Suspense, lazy } from "react";
      
      const Content = lazy(() => import("./Content"));
      
      export default function App() {
        return (
          <html>
            <head></head>
            <body>
              <div>App shell</div>
              <Suspense>
                <Content />
              </Suspense>
            </body>
          </html>
        );
      }
      

      第 1 次訪問頁面時,SSR 渲染的結果會分成 2 段傳輸,傳輸的第 1 段數據,經過格式化后,如下:

      <!DOCTYPE html>
      <html>
         <head></head>
         <body>
            <div>App shell</div>
            <!--$?-->
            <template id="B:0"></template>
            <!--/$-->
         </body>
      </html>
      

      其中 template 標簽的用途是為后續傳輸的 Suspense 的 children 渲染結果占位,注釋  和  中間的內容,表示是異步渲染出來的。

      傳輸的第 2 段數據,經過格式化后,如下:

      <div hidden id="S:0"> 
        <div> This is content </div>
      </div>
      <script> 
        function $RC(a, b) {
          a = document.getElementById(a);
          b = document.getElementById(b);
          b.parentNode.removeChild(b);
          if (a) {
              a = a.previousSibling;
              var f = a.parentNode,
                  c = a.nextSibling,
                  e = 0;
              do {
                  if (c && 8 === c.nodeType) {
                      var d = c.data;
                      if ("/$" === d)
                          if (0 === e) break;
                          else e--;
                      else "$" !== d && "$?" !== d && "$!" !== d || e++
                  }
                  d = c.nextSibling;
                  f.removeChild(c);
                  c = d
              } while (c);
              for (; b.firstChild;) f.insertBefore(b.firstChild, c);
              a.data = "$";
              a._reactRetry && a._reactRetry()
          }
        };
        $RC("B:0", "S:0") 
      </script>
      

      id="S:0" 的 div 正是 Suspense 的 children 的渲染結果,但是這個 div 設置了 hidden 屬性。接下來的 $RC 函數,會負責將這個 div 插入到第 1 段數據中 template 標簽所在的位置,同時刪除 template 標簽。

      總結一下 React Streaming SSR ,會先傳輸所有  以上層級的可以同步渲染得到的 html 結構,當  內的組件渲染完成后,會把這部分組件對應的渲染結果,連同一個 JS 函數再傳輸到瀏覽器端,這個 JS 函數會更新 dom ,得到最終的完整 HTML 結構。

      當第 2 次訪問頁面時,html 結構會一次性返回,而不會分成 2 次傳輸。這時候  組件為什么沒有將傳輸的數據分段呢?這是因為第 1 次請求時, Content 組件對應的 JS 模塊在服務器端已經被加載到模塊緩存中,再次請求時,加載 Content組件是一個同步過程,所以整個渲染過程是同步的,不存在分段傳輸渲染結果的情況。由此可見,只有當 的 children,需要被異步渲染時,SSR 返回的 HTML 才會被分段傳輸。

      除了動態加載 JS 模塊(code splitting)會產生分段傳輸數據的效果外,組件內獲取異步數據則是更加常見的適用 Streaming SSR 的場景。

      我們將 Content 組件做改造,通過調用異步函數 getData 獲取數據:

      let data;
      const getData = () => {
        if (!data) {
          data = new Promise((resolve) => {
            // 延遲 2s 返回數據
            setTimeout(() => {
              data = "content from remote";
              resolve();
            }, 2000);
          });
          throw data;
        }
      
        // promise-like
        if (data && data.then) {
          throw data;
        }
      
        const result = data;
        data = undefined;
        return result;
      };
      
      export default function Content() {
        // 獲取異步數據
        const data = getData();
        
        return <div>{data}</div>;
      }
      

      這樣,Content 的內容會延遲 2s,待獲取到 data 數據后傳輸到瀏覽器顯示。示例代碼(codesandbox 最近升級了,在 html 的 head 里注入了會阻塞 DOM 渲染的 JS,導致 Streaming SSR 效果可能失效,可以把代碼復制到本地測試)。

      注意:在數據未準備好前,getData 必須 throw 一個 promise,promise 會被 Suspense 組件捕獲,這樣才能保證 Streaming SSR 的順利執行。

      Selective Hydration

      React 18 之前,SSR 實際上是不支持 code splitting 的,只能使用一些 workaround,常見的方式有:1. 對于需要 code splitting 的組件,不在服務端渲染,而是在瀏覽器端渲染;2. 提前將 code splitting 的 JS 寫到 html script 標簽中,在客戶端等待所有的 JS 加載完成后再執行 hydration。

      這一點 React Team 的 Dan 在 Suspense 的 RFC 中也有提及:

      To the best of our knowledge, even popular workarounds forced you to choose between either opting out of SSR for code-split components or hydrating them after all their code loads, somewhat defeating the purpose of code splitting.

      當前 Modern.js 對于這種情況的處理,采用的是第 2 種方式。Modern.js 利用 @loadable/component 在 SSR 階段,收集做了 code splitting 的組件的 JS bundle,然后把這些 JS bundle 添加到 html script 標簽中,@loadable/component 提供了一個 API loadableReady ,在等待 JS bundle 加載完成后,才執行 hydration 。示意代碼如下:

      loadableReady(function(){
        hydrateRoot(root, <App/>)
      })
      

      如果在沒有等待所有的 JS bundle 都加載完成,就開始 hydration,會出現什么問題呢?

      考慮下面的例子,Content 組件做了 code splitting,如果在瀏覽端,在 Content 組件的 JS bundle 還未加載完成時,就開始 hydration,hydration 得到的 HTML 結構將缺少 Content 組件的內容,而服務端 SSR 返回的結構則是包含 Content 組件的,導致如下報錯:

      Hydration failed because the initial UI does not match what was rendered on the server.

      import loadable from '@loadable/component'
      
      const Content = loadable(() => import("./Content"));
      
      export default function App() {
        return (
          <html>
            <head></head>
            <body>
              <div>App shell</div>
              <Content />
            </body>
          </html>
        );
      }
      

      把上面的代碼,用 React 18 的 lazy 和 Suspense 改寫,就可以支持 Selective Hydration,使得 SSR 真正支持 code splitting:

      import {lazy, Suspense} from 'react'
      
      const Content = lazy(() => import("./Content"));
      
      export default function App() {
        return (
          <html>
            <head></head>
            <body>
              <div>App shell</div>
              <Suspense>
                <Content />
              </Suspense>
            </body>
          </html>
        );
      }
      

      如果 Content 組件的 JS bundle 還沒有加載完成,在 hydration 階段,渲染到 Suspense 節點時會跳出,而不會讓整個 hydration 過程失敗。

      Selective Hydration 還有另外一種使用場景:同步導入 Content 組件(不做 code splitting),但是需要注意 Content 組件內仍然有異步的讀取數據操作(見上文代碼),另外增加一個 SideBar 組件,用于驗證事件綁定,代碼如下:

      import {lazy, Suspense, useState} from 'react'
      // 同步導入 Content 組件
      import Content from './Content';
      
      const Sidebar = () => {
        const [color, setColor] = useState('black');
        return (
          <div className="home">
            <div style={{ color }}>Siderbar</div>
            <button
              onClick={() => {
                setColor(color === 'black' ? 'red' : 'black');
              }}
            >
              change
            </button>
          </div>
        );
      };
      
      export default function App() {
        return (
          <html>
            <head></head>
            <body>
              <div>App shell</div>
              <Sidebar />
              <Suspense>
                <Content />
              </Suspense>
            </body>
          </html>
        );
      }
      

      訪問頁面時,在渲染出 Content 組件前,Siderbar 就已經可以交互了(點擊 change 按鈕,文字顏色會改變)。說明,雖然所有組件使用一個 JS bundle 做 hydration,但是如果 Suspense 內的組件沒有完成渲染,并不會影響其他已經渲染出的組件做 hydration。示例代碼。

      總結一 下,React 18 的 hydration 階段,當渲染到 Suspense 組件時,會根據 Suspense 的 children 是否已經渲染完成,而選擇是否繼續向子組件執行 hydration。未渲染完成的組件待渲染完成后,會恢復執行 hydration。 Suspense 的 children 異步渲染的兩種場景:1. children 組件做了 code splitting;2. children 組件中有異步操作。

      降級邏輯

      Streaming SSR 過程中,如果某個 Suspense 的 children 渲染過程拋出異常,那么這個 children 組件將降級到 CSR,即在瀏覽器端重新嘗試渲染。

      例如,我們對前面使用的 Content 組件做改造,刻意在服務端 SSR 階段拋出異常:

      export default function Content() {
        const _data = getData();
        // 制造異常
        if(typeof window === 'undefined'){
          data = undefined
          throw Error('SSR Error')
        }
      
        return (
          <div>
            {_data}
          </div>
        );
      }
      

      訪問頁面時,Response 返回的第二段數據,格式化后如下所示:

      <script>
        function $RX(b, c, d, e) {
          var a = document.getElementById(b);
          b = a.previousSibling;
          b.data = "$!";
          a = a.dataset;
          c && (a.dgst = c);
          d && (a.msg = d);
          e && (a.stck = e);
          b._reactRetry && b._reactRetry()
        };
        $RX("B:0", "", "SSR Error", "\n    at Content\n    at Lazy\n    at Content\n    at Lazy\n    at Suspense\n    at body\n    at html\n    at App\n    at DataProvider (/Users/bytedance/work/examples/stream-ssr-demo/src/data.js:18:23)") 
       </script>
      

      第二段數據中返回了 $RX 函數,而不是渲染正確情況下的  $RC 函數。$RX 會將渲染出錯的 Suspense 在 HTML 中對應的 Comment 標簽  修改為 ,表示這個 Suspense 的 children 需要在瀏覽器端執行降級渲染。當執行 $RX 時,如果父組件已經完成 hydration,會調用 Comment 節點上的 _reactRetry 方法,立即執行對需要降級的組件的渲染;否則等待父組件執行時 hydration,再“順道”執行渲染。

      當 Suspense 的 children SSR 階段渲染失敗時,可以在 renderToPipeableStream 的 onError 回調中執行專門的邏輯處理,例如下面的例子中,會打印出錯誤日志,并將響應的狀態碼設置為 500。

      如果還沒有渲染到任一 Suspense 組件時,就發生了錯誤,這意味著應用對應的整棵組件樹都沒有渲染成功,SSR 完全失敗,這個時候 onShellReady 不會被調用,onShellError 會調用,我們可以在 onShellError 中返回 CSR 使用的 HTML 模版,讓整個應用完全降級到 CSR 。

       let didError = false;
       const stream = renderToPipeableStream(
          <App assets={assets} />,
          {
            onShellReady() {
              // If something errored before we started streaming, we set the error code appropriately.
              res.statusCode = didError ? 500 : 200;
              res.setHeader("Content-type", "text/html");
              stream.pipe(res);
            },
            onError(x) {
              didError = true;
              console.error(x);
            },
            onShellError(x) {
              didError = true;
              res.send(<html>...</html>)//返回 CSR 使用的 HTML 模版,整棵組件樹降級到 CSR  
            }
          }
        );
      

      JS 和 CSS 設置

      當前,我們還沒有介紹如何在 Streaming SSR 中設置 JS 和 CSS 文件。有三種方式:

      1. 在 HTML 組件中設置示例如下:
      function Html({ assets, children, title }) {
          return (
            <html>
              <head>
                <title>{title}</title>
                <link rel="stylesheet" href={assets["main.css"]} />
                <script src={assets["main.js"]}></script>
              </head>
              <body>
                <noscript
                  dangerouslySetInnerHTML={{
                    __html: `<b>Enable JavaScript to run this app.</b>`
                  }}
                />
                {children}
                <script
                  dangerouslySetInnerHTML={{
                    __html: `assetManifest = ${JSON.stringify(assets)};`
                  }}
                />
              </body>
            </html>
          );
        }
        
      function App({assets}) {
         return (
           <Html assets={assets} title="Hello">
              {/* other components */}
           </Html>
         );
       }
       
        hydrateRoot(document, <App assets={window.assetManifest} />);
      

      我們將 html、head、body 等這些標簽也通過 React 組件表示,這樣對 JS 和 CSS 的設置,也可以在 JSX 中完成。示例中,通過 assets 屬性,設置 HTML 組件需要引人的 JS 和 CSS 文件。 SSR 階段時,assets 一般是通過讀取 webpack 等構建工具的構建產物結果得到的,assets 還會寫入到一個 script 的assetManifest 變量上, 這樣在 hydration 階段,App 組件可以通過 window.assetManifest 獲取到 assets 信息。

      1. 在返回第一段數據時添加這種方式下,html、head、body 等這些最外層標簽,通過 HTML 模版注入到 Streaming SSR 返回的第一段數據中。 示例如下:
      import { Transform } from 'stream';
      
      // 代表傳輸的第一段數據
      let isShellStream = true;
      const injectTemplateTransform = new Transform({
        transform(chunk, _encoding, callback) {
          if (isShellStream) {
            // headTpl 代表 <html><head>...</head><body><div id='root'> 部分的模版
            // tailTpl 代表 </div></body></html> 部分的模版
            this.push(`${headTpl}${chunk.toString()}${tailTpl}`));
            isShellStream = false;
          } else {
            this.push(chunk);
          }
          callback();
        },
      });
      
      const stream = renderToPipeableStream(
        <App />,
        { 
          onShellReady() {
            res.setHeader('Content-type', 'text/html');
            stream.pipe(injectTemplateTransform).pipe(res);
          },
        }
      );
      

      在構建階段,將 HTML 所需的 JS 和 CSS 文件,構建到 html 模版中。然后通過創建一個 Transform 流,在傳輸第一段數據時,將 headTpl 、tailTpl 的 html 模版數據添加到第一段數據的兩端。

      1. 通過參數 bootstrapScripts 設置通過 renderToPipeableStream 的第二個參數,設置 bootstrapScripts 的值,``bootstrapScripts` 的值為 HTML 所需的 JS 文件路徑。注意,這種方式不支持設置 CSS 文件。 示例如下:
      const stream = renderToPipeableStream(
        <App />,
        { 
          bootstrapScripts: ["main.js"],
          onShellReady() {
            res.setHeader('Content-type', 'text/html');
            stream.pipe(res);
          },
        }
      );
      

      源碼解析

      數據結構

      Streaming SSR 的實現,主要涉及 Segment、Boundary、Task 和 Request 4種數據結構。

      Segment

      代表 Streaming SSR 分段傳輸過程中的每段數據。

      簡化后的 Segment 類型及字段說明如下:

      type Segment = {
        // segment 狀態。依次代表 pending、completed、flushed、aborted、errored
        status: 0 | 1 | 2 | 3 | 4, 
        
        // 真正要傳輸到瀏覽器端的數據
        chunks: Array<string | Uint8Array>,
        
        // 子級 Segment,當遇到 Suspense Boundary 時會創建新的 Segment,
        // 作為當前 Segment 的子級 Segment 
        children: Array<Segment>,  
        // 在父級 Segment 的 chunks 中的位置索引,如果沒有父級 Segment, 則為 0
        index: number,
       
        // 如果這個 Segment 代表 Suspense 組件的 fallback, 
        // boundary 代表 Suspense 組件內部真正內容對應的 Boundary
        boundary: null | SuspenseBoundary,
      
      };
      
      • status

      新建時,狀態為 pending;當 Segment 已經獲取到需要傳輸的數據時,狀態為 completed;當 Segment 的數據已經寫入到 HTTP Response 對象時,狀態為 flushed。

      • children

      當 React 解析到 Suspense 組件時,會創建新的 Segment,存儲到當前 Segment 的 children 中。 例如以下 App 組件:

      import { lazy } from 'react'
      
      const Content = lazy(() => import('./Content' ));
      
      function App = (props) => {
        return (
          <div>
            <div>App</div> 
            <Suspense fallback={<Spinner />}>
               <Content />
            </Suspense>
          </div>
        )
      }
      

      React 會創建 3 個 Segment:

      Segment 1 對應的 DOM 結構為:

      <div>
        <div>App<div/> 
      </div>
      

      Segement 1 對應所有 Suspense 組件之上的內容,可以稱為 Root Segment

      Segment 2 對應 Spinner 組件渲染出的內容。同時 Segment 2 會存儲到 Segment 1 的 children 屬性中。

      Segment 3 對應 Suspense 組件的 children 渲染出的內容。注意,因為被 Suspense 組件分割,Segment 3 的內容和 Segment 1 、Segment 2 的內容,在 HTTP 傳輸過程中,是分成 2 段傳輸的(也有可能是在 1 段中傳輸,后面會介紹),所以 Segment 3 并不會保存到 Segment 1 的 children中。

      • index

      繼續考慮上面的例子,Segment 1  chunks 保存的數組元素,我們做一下簡化,用以下 3 個元素示意

      [0]: <div>
      [1]: <div>App</div> 
      [2]: </div>
      

      Segment 2 chunks 中的數據,需要插入到 Segment 1 chunks 數組中的第 1 個元素之后的位置,才能保證傳輸的 dom 結構順序是正確的,所以這個例子中 index 等于 2 。

      Boundary

      SSR 邏輯分段的“分界線”,每個 Suspense 組件對應 1 個 Suspense Boundary。

      例如以下 App 組件有 2 個 Suspense 組件,會創建 2 個 Boundary,這 2 個 Boundary 實際上將整個組件的解析過程分成了 3 部分,Boundary 1 以上的部分,我們也可以視做一個 Boundary,稱為 Root Boundary。

      import { lazy } from 'react'
      
      const Content = lazy(() => import('./Content' ));
      const Comments = lazy(() => import('./Comments' ));
      
      function App = (props) => {
        return (
          <div>
            <div>App<div/> 
            {/* Boundary 1 */}
            <Suspense fallback={<Spinner />}>    
               <Content />
               {/* Boundary 2 */}
               <Suspense fallback={<Spinner />}>
                 <Comments />
               </Suspense>
            </Suspense>
          </div>
        )
      }
      

      簡化后的 Boundary ( React 代碼中命名為 SuspenseBoundary)類型及字段說明如下:

      type SuspenseBoundary = {
        // 當前 boundary 范圍內的 pending 狀態的 task 數量
        pendingTasks: number, 
        // 當前 boundary 范圍內的已完成渲染的 Segment  
        completedSegments: Array<Segment>, 
      };
      

      Task

      1 個 Task 代表一個將組件樹渲染成 DOM 結構的任務。一般情況下,一個應用對應一棵組件樹,似乎一個應用只需要 1 個 Task 即可。但是,因為 Suspense 將組件樹分成了多個子組件樹,子組件樹可以是異步處理的,所以實際上會需要多個 Task。

      簡化后的 Task 類型及字段說明如下:

      type Task = {
        // Task 對應的組件樹
        node: ReactNodeList,
        
        // Task 對應的 Boundary
        blockedBoundary: null | SuspenseBoundary,
        // Task 對應的 Segment
        blockedSegment: Segment,    
        
        // 后面介紹
        ping: () => void,
      }
      

      blockedBoundary 的值可以為 null 或 SuspenseBoundary。 null 表示 task 代表所有 Suspense 組件之上的組件樹的渲染任務,即 root task;

      SuspenseBoundary 表示 task 代表某個 Suspense 組件內的組件樹的異步渲染任務。

      通過如下示例進一步說明:

      import { lazy } from 'react'
      
      const Content = lazy(() => import('./Content' ));
      
      function App = (props) => {
        return (
          <div>
            <div>App</div> 
            <Suspense fallback={<Spinner />}>
               <Content />
            </Suspense>
          </div>
        )
      }
      

      在 SSR 渲染開始時,會創建一個 Task,代表 App 作為根節點的組件樹的渲染任務。這個 Task 的 Boundary 為 Root Boundary,所以為 null。

      如果是第一次請求,因為 Content 組件做了 code splitting,所以 Content 組件代碼的加載是異步的。這時會再創建 2 個 Task,一個為代表包裹 Content 組件的 React.lazy 為根節點的組件樹的渲染任務;另一個為代表 Spinner 作為根節點的組件樹的渲染任務。

      這種情況,SSR 渲染結果會分成 2 次傳輸。

      如果不是第一次請求,這是 Content 模塊已經被加載到緩存中,再次加載不存在異步問題。此時,整個組件樹的渲染是一個同步過程,也不需要使用 fallback 組件 Spinner  ,所以只需要一個 Task 即可,即 App 作為根節點的 Task。

      這種情況,SSR 渲染結果只需要 1 次傳輸。

      Request

      Request 是 SSR 邏輯中的最頂層對象。每 1 個 SSR 請求,會生成一個 Request 對象,存儲這次 SSR 過程所需要的 Task、Boundary、Segement 等相關信息,以及 SSR 過程中不同時機的回調函數(onShellReady ,onAllReady ,onShellError,onError )。

      簡化后的 Request 類型及字段說明如下:

      type Request = {
        // 請求結果的輸出流,即 Response 對象
        destination: null | Destination,
      
        // 所有未完成的 Task 數量,當等于 0 時,表示本次 SSR 完成,可以關閉 HTTP 連接
        allPendingTasks: number, 
        // Root Boundary 范圍內的未完成的 Task 數量,當等于 0 時,Root Boundary 渲染完成
        pendingRootTasks: number, 
        // 等待執行的 Task
        pingedTasks: Array<Task>,
        
        // 已完成的 Root Segment 
        completedRootSegment: null | Segment, 
       
        // 已完成的 Boundary 
        completedBoundaries: Array<SuspenseBoundary>, 
        
        // Root Boundary 渲染完成后的回調
        onShellReady: () => void,
        // Root Boundary 渲染過程中,出錯的回調
        onShellError: (error: mixed) => void,
        // 所有 Boundary 都渲染完成,即 SSR 完成的回調
        onAllReady: () => void,
        // Root Boundary 渲染完成后,在后續 Suspense Boundary 渲染過程中出錯的回調
        onError: (error: mixed) => ?string,
      
      };
      

      主要流程

      renderToPipeableStream 涉及的關鍵函數調用過程如下圖所示:

      renderToPipeableStream 的關鍵代碼如下:

      function renderToPipeableStream(
        children: ReactNodeList,
        options?: Options,
      ): PipeableStream {
        // 創建請求對象 Request
        const request = createRequest(children, options);
        // 啟動組件樹的渲染任務
        startWork(request);
        
        return {
          pipe<T: Writable>(destination: T): T {
            // 開始將渲染結果寫入輸出流 
            startFlowing(request, destination);
            return destination;
          },
          abort(reason: mixed) {
            abort(request, reason);
          },
        };
      }
      

      為了便于理解主干流程,本節列出的 React 源碼,做了大量刪減和微調,并非完整源碼。

      完整源碼請參考:ReactDOMFizzServerNode.js 、ReactFizzServer.js、 ReactServerStreamConfigNode.js 等文件。

      分析上面的代碼調用過程,我們把 SSR 過程分為三個階段:

      1. 創建請求對象創建請求對象即創建 Request 數據結構,對應 createRequest ,主要邏輯為:a. 根據入參 options ,創建 request 對象,設置 onShellReady 、onAllReady 等回調函數b. 創建 root segment,關聯的 boundary 為 root boundary,即 nullc. 根據入參 children 和 root segment,創建 root taskd.   將 root task 保存到 request 的 pingedTasks 中,root task 將作為后續渲染操作的起點
      export function createRequest(
        children: ReactNodeList,
        options?: Options,
      ): Request {
        const pingedTasks = [];
        
        const request = {
          //  初始化 request
        };
        
        // This segment represents the root fallback.
        const rootSegment = {
          status: PENDING,
          index: 0,
          chunks: [],
          children: [],
        };
        
        const rootTask = createTask(
          request,
          children,
          null,
          rootSegment
        );
        
        pingedTasks.push(rootTask);
        return request;
      }
      

      Root task 由createTask 創建,創建 task 時,需要設置 task 關聯的待渲染的組件樹( node )、 Boundary( blockedBoundary ) 和 Segement ( blockedSegment ),同時還需要修改 request和 blockedBoundary 關聯的待完成的 task 數量。

      createTask 簡化后的代碼及注釋如下:

      function createTask(
        request: Request,
        node: ReactNodeList,
        blockedBoundary: Root | SuspenseBoundary,
        blockedSegment: Segment,
      ): Task {
        // allPendingTasks 自增 1
        request.allPendingTasks++;
        
        // 如果是 root boundary, pendingRootTasks 自增1;
        // 否則把對應 boundary 范圍里的 pendingTask 自增1
        if (blockedBoundary === null) {
          request.pendingRootTasks++;
        } else {
          blockedBoundary.pendingTasks++;
        }
        
        // 創建 task,ping 的作用后續介紹
        const task: Task = ({
          node,
          ping: () => pingTask(request, task),
          blockedBoundary,
          blockedSegment,
        }: any);
      
        return task;
      }
      

      **
      **2、啟動渲染流程
      創建好 root task 后,就可以以 root task 作為起點,啟動組件的渲染流程了,對應 startWork 。

      主要邏輯可以從 startWork 內部調用performWork 開始看:

      export function performWork(request: Request): void {
        const pingedTasks = request.pingedTasks;
        let i;
        for (i = 0; i < pingedTasks.length; i++) {
          const task = pingedTasks[i];
          retryTask(request, task);
        }
        pingedTasks.splice(0, i);
        if (request.destination !== null) {
          flushCompletedQueues(request, request.destination);
        }
      }
      

      performWork遍歷 request 的pingedTasks,對每一個 task 執行 retryTask 。retryTask 主要邏輯如下:

      1. 通過調用 renderNodeDestructive ,對 task 包含的 React node 節點執行渲染邏輯。
      2. 如果renderNodeDestructive 執行過程中沒有拋出異常:a.   表示 task 關聯的渲染任務完成,將 task 關聯的 segment 狀態設置為完成狀態。b. 調用 finishedTask ,對 request 上的 segment 信息做更新:如果是 root boundary 的task,將當前 task 關聯的 segment 賦值給 request 的completedRootSegment ;如果是 suspense boundary,將當前 task 關聯的 segment 添加到關聯 boundary 的 completedSegments。注意,onShellReady 回調也是在這個函數中執行的,當 root boundary 上的 task 都已經執行完成(request.pendingRootTasks === 0),就會調用onShellReady 。
      3. 如果renderNodeDestructive 執行過程中拋出異常(主要針對 throw promise 場景):a.   捕獲異常,如果是 promise-like 對象,在 promise resolve 后,把當前 task 重新放到 request 的 pingedTask 中,等待重新執行(調用 performWork )。

      retryTask 主要代碼如下:

      function retryTask(request: Request, task: Task): void {
        const segment = task.blockedSegment;
      
        try {
          renderNodeDestructive(request, task, task.node);
          segment.status = COMPLETED;
          finishedTask(request, task.blockedBoundary, segment);
        } catch (x) {
          resetHooksState();
          if (typeof x === 'object' && x !== null && typeof x.then === 'function') {
            // Something suspended again, let's pick it back up later.
            const ping = task.ping;
            x.then(ping, ping);
          }
        }
      }
      

      3.a 步驟中,需要依賴 12行的 task.ping 把 task 重新放回 request 的 pingedTasks。 task.ping 對應函數:() => pingTask(request, task),pingTask 實現如下:

      function pingTask(request: Request, task: Task): void {
        const pingedTasks = request.pingedTasks;
        pingedTasks.push(task);
        scheduleWork(() => performWork(request));
      }
      

      renderNodeDestructive 對 task 的 node 屬性代表的組件樹,做 深度優先 遍歷,一邊將組件渲染為 dom 節點,一邊將 dom 節點的信息存儲到 task 的 blockedSegment  屬性中。

      Streaming SSR 實現的一個關鍵,是對Suspense組件的渲染邏輯。當 renderNodeDestructive 遍歷到 Suspense 組件時,會調用renderSuspenseBoundary 執行渲染邏輯。

      renderSuspenseBoundary 的主要邏輯為:

      1. 針對解析到的 Suspense 組件,創建一個新的 Boundary:newBoundary
      2. 新建一個 segment:boundarySegment, boundarySegment用于保存 Suspense 的 fallback 代表的內容,所以boundarySegment 的 boundary 屬性值為 newBoundary 。同時, boundarySegment 也會保存到當前 task 的 blockedSegment的 children 屬性中(可參考介紹 Segment 數據結構的例子)。
      3. 新建一個 segment:contentRootSegment ,保存 Suspense組件的children 代表的內容。
      4. 渲染 Suspense組件的children
      5. 如果渲染成功,說明 Suspense組件的 children沒有需要異步等待的內容(渲染是同步完成的):a.   設置contentRootSegment 的狀態為 COMPLETEDb.  把 contentRootSegment存入newBoundary的completedSegments屬性中
      6. 如果渲染過程 throw promise,說明 Suspense的 children 有需要異步等待的內容:a.   新建一個 task,task 的blockedBoundary等于 newBoundaryb. 當 promise resolve 后,將 task 保存到 request 的 pingedTasks 中(通過 task 的ping屬性),等待下一個事件循環處理。c. 再新建一個 task,代表Suspense 的 fallback 組件樹的渲染任務, task 的blockedSegment 等于 boundarySegment,task 的blockedBoundary 等于調用 renderSuspenseBoundary時的 task.blockedBoundary (不是 newBoundary,是 newBoundary 上一層級的 boundary)d. 把 task 保存到 request的 pingedTasks 中,等待在 performWork 中處理

      這段邏輯比較復雜,簡單理解的話,在渲染過程中,每當遇到 Suspense 組件,就會創建一個新的 Boundary,但新 Boundary 并不意味著一定要創建一個新的 Task,因為Suspense組件內元素的渲染不一定需要異步完成,只有存在 動態導入組件(React.lazy)或獲取異步數據等情況,才會創建一個新的 Task,用以表示這個異步的渲染過程。

      上面的過程還有 2 個注意點:

      1. 步驟 6.a 中,新建的 task 不會立即放入 request的 pingedTasks 中,而是要等待代表異步任務的 promise resolve 后,才放入pingedTasks。所以 pingedTasks ,實際上保存的是「沒有異步任務依賴」的 task,是可以同步完成組件渲染工作的 task。
      2. 步驟 5 中, 沒有 6.c 和 6.d 兩步, 因為如果 Suspense 的 children 沒有需要異步等待的內容,就不需要展示 fallback 內容,自然也不需要新建一個 task 負責 fallback 組件樹的渲染任務 。

      3、啟動輸出流

      renderToPipeableStream 返回 pipe 和 abort 2 個方法,分別用于向輸出流寫入組件樹的渲染結果,和終止本次 SSR 請求。這里我們主要分析向輸出流寫入組件樹的渲染結果。pipe 內部調用startFlowing,startFlowing 調用 flushCompletedQueues,flushCompletedQueues顧名思義,會將已完成的組件樹的渲染信息,寫入到輸出流(Response)。

      flushCompletedQueues 主要邏輯為:

      1. 檢測 root boundary 范圍的 tasks 是否已經渲染完成,如果是,則將對應的 segments 寫入輸出流;如果否,則返回(因為需要保證寫入輸出流的第一段數據,一定是 root boundary 范圍內的組件的渲染結果)
      2. 檢查 suspense boundaries ,如果 suspense boundary 滿足條件:關聯的所有 task 都已經完成, 則將 suspense boundary 的 segment 寫入輸出流,suspense boundary 的完整內容在瀏覽器頁面處于可見狀態(不再顯示 suspense 的 fallback 內容)。
      3. 繼續檢查 suspense boundaries,如果 suspense boundary 滿足條件:存在完成的 task,但不是所有 task 都完成,則將這些完成的 task 的 segment 寫入輸出流,但 suspense boundary 的完整內容在瀏覽器頁面仍然處于隱藏狀態(包裹內容的 div 此時還是 hidden 狀態)。
      4. 如果所有 suspense boundaries 的關聯的 task 都已經完成,說明本次 SSR 完成, 調用 close 結束請求。

      flushCompletedQueues 簡化后的代碼如下:

      function flushCompletedQueues(
        request: Request,
        destination: Destination,
      ): void {
      
          // 1.開始:root boundary 寫入到輸出流
          beginWriting(destination);
       
          let i;
          const completedRootSegment = request.completedRootSegment;
          if (completedRootSegment !== null) {
            // 將 root boundary 范圍內的組件渲染結果寫入輸出流
            if (request.pendingRootTasks === 0) {
              flushSegment(request, destination, completedRootSegment);
              request.completedRootSegment = null;
              writeCompletedRoot(destination, request.responseState);
            } else {
              // root boundary 范圍內,還存在沒有完成的 task,直接返回。
              // 不需要繼續向下看 suspense boundary 是否完成
              return;
            }
          }
          
          // 1.完成:root boundary 寫入到輸出流
          completeWriting(destination);
          
          // 2.開始:suspense boundary(關聯的 task 已全部完成)寫入到輸出流
          beginWriting(destination);
      
          const completedBoundaries = request.completedBoundaries;
          for (i = 0; i < completedBoundaries.length; i++) {
            const boundary = completedBoundaries[i];
            if (!flushCompletedBoundary(request, destination, boundary)) {
              request.destination = null;
              i++;
              completedBoundaries.splice(0, i);
              return;
            }
          }
          completedBoundaries.splice(0, i);
      
          // 2.完成:suspense boundary(關聯的 task 已全部完成)寫入到輸出流
          completeWriting(destination);
          
          // 3.開始:suspense boundary(關聯的 task 部分完成)寫入到輸出流
          beginWriting(destination);
      
          const partialBoundaries = request.partialBoundaries;
          for (i = 0; i < partialBoundaries.length; i++) {
            const boundary = partialBoundaries[i];
            if (!flushPartialBoundary(request, destination, boundary)) {
              request.destination = null;
              i++;
              partialBoundaries.splice(0, i);
              return;
            }
          }
          partialBoundaries.splice(0, i);
          
          // 3.完成:suspense boundary(關聯的 task 部分完成)寫入到輸出流
          completeWriting(destination);
          
          if (
            request.allPendingTasks === 0 &&
            request.pingedTasks.length === 0 &&
            request.clientRenderedBoundaries.length === 0 &&
            request.completedBoundaries.length === 0
          ) {  
            // 所有渲染任務都已完成,關閉輸出流
            close(destination);
          }
      
      }
      

      上面的代碼中,一共有 3 組 beginWriting / completeWriting ,分別代表了 flushCompletedQueues 的前 3 步驟。

      至此,我們就完成了 Streaming SSR 主要源碼實現的分析。

      posted @ 2023-01-03 17:45  字節跳動終端技術  閱讀(1225)  評論(0)    收藏  舉報
      主站蜘蛛池模板: 最新偷拍一区二区三区| 国产精品亚洲精品日韩已满十八小| 欧洲一区二区中文字幕| 高清在线一区二区三区视频| 高清无码在线视频| 大姚县| 国产短视频精品一区二区| 人妻一本久道久久综合鬼色| 加勒比无码专区中文字幕| 章丘市| 亚洲成人免费一级av| 国产精品无码a∨麻豆| 免费无码黄十八禁网站| 欧美不卡无线在线一二三区观| 亚洲18禁一区二区三区| 成年女人片免费视频播放A| 日日噜噜噜夜夜爽爽狠狠视频 | 国产高清精品在线91| 丰满人妻一区二区三区高清精品| 武宁县| 国产精品美女免费无遮挡| 亚洲av永久无码精品天堂久久| 欧美精品v国产精品v日韩精品| 中文字幕乱偷无码av先锋蜜桃| 国产真人做受视频在线观看| 亚洲av影院一区二区三区| 人妻久久久一区二区三区| 亚洲午夜久久久久久噜噜噜| 亚洲影院丰满少妇中文字幕无码 | 国产精品亚洲一区二区z| 国产精品妇女一区二区三区| 亚洲中文字幕无码专区| 国产精品福利中文字幕| 日本污视频在线观看| 中文字幕在线永久免费视频| 国产另类ts人妖一区二区| 亚洲国产精品一区在线看| 性欧美VIDEOFREE高清大喷水| 激情 小说 亚洲 图片 伦| 国产漂亮白嫩美女在线观看| 亚洲中文字幕一二三四区|