renderToPipeableStream

renderToPipeableStream 將 React 樹狀結構渲染成可管道化的 Node.js 串流。

const { pipe, abort } = renderToPipeableStream(reactNode, options?)

注意事項

此 API 適用於 Node.js。具有 Web Streams 的環境(例如 Deno 和現代邊緣執行環境)應改用 renderToReadableStream


參考

renderToPipeableStream(reactNode, options?)

呼叫 renderToPipeableStream 將 React 樹狀結構以 HTML 形式渲染到 Node.js 串流。

import { renderToPipeableStream } from 'react-dom/server';

const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
}
});

在客戶端上,呼叫 hydrateRoot 使伺服器生成的 HTML 具有互動性。

請參閱以下更多範例。

參數

  • reactNode:您想要渲染成 HTML 的 React 節點。例如,像 <App /> 這樣的 JSX 元素。它應該代表整個文件,因此 App 元件應該渲染 <html> 標籤。

  • **選用** options:具有串流選項的物件。

    • **選用** bootstrapScriptContent:如果指定,此字串將放置在內嵌的 <script> 標籤中。
    • **選用** bootstrapScripts:頁面上要發出的 <script> 標籤的字串 URL 陣列。使用它來包含呼叫 hydrateRoot<script>。如果您根本不想在客戶端上執行 React,請省略它。
    • **選用** bootstrapModules:類似於 bootstrapScripts,但發出 <script type="module">
    • **選用** identifierPrefix:React 用於 useId 生成的 ID 的字串前綴。在同一個頁面上使用多個根目錄時,這有助於避免衝突。必須與傳遞給 hydrateRoot 的前綴相同。
    • 選用 namespaceURI:一個包含根 命名空間 URI 的字串,用於串流。預設為一般 HTML。傳入 'http://www.w3.org/2000/svg' 代表 SVG,或傳入 'http://www.w3.org/1998/Math/MathML' 代表 MathML。
    • 選用 nonce:一個 nonce 字串,允許 script-src 內容安全策略 (Content-Security-Policy) 的腳本。
    • 選用 onAllReady:當所有渲染完成時觸發的回調函式,包含 初始框架 (shell) 和所有額外的 內容。您可以使用它來取代 onShellReady ,以便爬蟲和靜態生成使用。如果您在此處開始串流,您將不會獲得任何漸進式加載。串流將包含最終的 HTML。
    • 選用 onError:每當發生伺服器錯誤時觸發的回調函式,無論是 可恢復的 還是 不可恢復的。預設情況下,這只會呼叫 console.error。如果您覆蓋它以 記錄崩潰報告,請確保您仍然呼叫 console.error。您也可以使用它在發出初始框架之前 調整狀態碼
    • 選用 onShellReady:在 初始框架 渲染完成後立即觸發的回調函式。您可以 設定狀態碼 並在此處呼叫 pipe 以開始串流。React 將會 在初始框架之後串流額外的內容,以及用於將 HTML 加載後備方案替換為內容的內聯 <script> 標籤。
    • 選用 onShellError:如果渲染初始框架時發生錯誤,則會觸發的回調函式。它會接收錯誤作為參數。此時串流尚未發出任何位元組,且 onShellReadyonAllReady 都將不會被呼叫,因此您可以 輸出後備 HTML 框架
    • 選用 progressiveChunkSize:區塊中的位元組數。閱讀更多關於預設啟發式演算法的資訊

回傳

renderToPipeableStream 會回傳一個物件,其中包含兩個方法

  • pipe 將 HTML 輸出到提供的 可寫入的 Node.js 串流。如果您想要啟用串流,請在 onShellReady 中呼叫 pipe;或者,針對爬蟲和靜態生成,在 onAllReady 中呼叫。
  • abort 允許您 中止伺服器渲染 並在客戶端上渲染其餘部分。

用法

將 React 樹狀結構作為 HTML 渲染到 Node.js 串流

呼叫 renderToPipeableStream 將您的 React 樹狀結構作為 HTML 渲染到 Node.js 串流

import { renderToPipeableStream } from 'react-dom/server';

// The route handler syntax depends on your backend framework
app.use('/', (request, response) => {
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
}
});
});

除了根組件之外,您還需要提供一個啟動 <script> 路徑的列表。您的根組件應該返回包含根 <html> 標籤的完整文件。

例如,它可能看起來像這樣

export default function App() {
return (
<html>
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="stylesheet" href="/styles.css"></link>
<title>My app</title>
</head>
<body>
<Router />
</body>
</html>
);
}

React 會將 DOCTYPE 和您的啟動 <script> 標籤 注入到產生的 HTML 串流中。

<!DOCTYPE html>
<html>
<!-- ... HTML from your components ... -->
</html>
<script src="/main.js" async=""></script>

在客戶端,您的啟動腳本應該使用 `hydrateRoot` 函式來水合整個 document

import { hydrateRoot } from 'react-dom/client';
import App from './App.js';

hydrateRoot(document, <App />);

這會將事件監聽器附加到伺服器生成的 HTML 並使其具有互動性。

深入探討

從建置輸出中讀取 CSS 和 JS 資源路徑

最終的資源網址(例如 JavaScript 和 CSS 檔案)通常在建置後會進行雜湊處理。例如,您可能會得到 styles.123456.css,而不是 styles.css。雜湊靜態資源檔名可確保相同資源的每個不同建置版本都具有不同的檔名。這很有用,因為它允許您安全地啟用靜態資源的長期快取:具有特定名稱的檔案內容永遠不會更改。

但是,如果您在建置完成後才知道資源網址,就無法將它們放入原始碼中。例如,像前面那樣將 "/styles.css" 硬編碼到 JSX 中將無法正常工作。為了將它們排除在您的原始碼之外,您的根組件可以從作為 prop 傳遞的映射中讀取實際檔名。

export default function App({ assetMap }) {
return (
<html>
<head>
...
<link rel="stylesheet" href={assetMap['styles.css']}></link>
...
</head>
...
</html>
);
}

在伺服器上,渲染 <App assetMap={assetMap} /> 並傳遞您的帶有資源網址的 assetMap

// You'd need to get this JSON from your build tooling, e.g. read it from the build output.
const assetMap = {
'styles.css': '/styles.123456.css',
'main.js': '/main.123456.js'
};

app.use('/', (request, response) => {
const { pipe } = renderToPipeableStream(<App assetMap={assetMap} />, {
bootstrapScripts: [assetMap['main.js']],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
}
});
});

由於您的伺服器現在正在渲染 <App assetMap={assetMap} />,因此您也需要在客戶端使用 assetMap 渲染它,以避免水合錯誤。您可以序列化 assetMap 並將其傳遞給客戶端,如下所示:

// You'd need to get this JSON from your build tooling.
const assetMap = {
'styles.css': '/styles.123456.css',
'main.js': '/main.123456.js'
};

app.use('/', (request, response) => {
const { pipe } = renderToPipeableStream(<App assetMap={assetMap} />, {
// Careful: It's safe to stringify() this because this data isn't user-generated.
bootstrapScriptContent: `window.assetMap = ${JSON.stringify(assetMap)};`,
bootstrapScripts: [assetMap['main.js']],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
}
});
});

在上面的範例中,bootstrapScriptContent 選項添加了一個額外的內聯 <script> 標籤,該標籤在客戶端上設置全局 window.assetMap 變數。這讓客戶端程式碼可以讀取相同的 assetMap

import { hydrateRoot } from 'react-dom/client';
import App from './App.js';

hydrateRoot(document, <App assetMap={window.assetMap} />);

客戶端和伺服器都使用相同的 assetMap prop 渲染 App,因此沒有水合錯誤。


在載入時串流更多內容

串流允許使用者在伺服器上載入所有數據之前就開始看到內容。例如,考慮一個顯示封面、帶有朋友和照片的側邊欄以及貼文列表的個人資料頁面。

function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Posts />
</ProfileLayout>
);
}

想像一下,載入 <Posts /> 的數據需要一些時間。理想情況下,您希望在不等待貼文的情況下向使用者顯示個人資料頁面的其餘內容。為此,請Posts 包裹在 <Suspense> 邊界中:

function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}

這會告訴 React 在 Posts 載入其數據之前開始串流 HTML。React 將首先發送載入後備 (PostsGlimmer) 的 HTML,然後,當 Posts 完成載入其數據後,React 將發送其餘的 HTML 以及一個內聯 <script> 標籤,該標籤會將載入後備替換為該 HTML。從使用者的角度來看,頁面將首先顯示 PostsGlimmer,稍後將由 Posts 替換。

您可以進一步嵌套 <Suspense> 邊界來創建更精細的載入順序。

function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<BigSpinner />}>
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</Suspense>
</ProfileLayout>
);
}

在此範例中,React 可以更早地開始串流頁面。只有 ProfileLayoutProfileCover 必須先完成渲染,因為它們沒有被任何 <Suspense> 邊界包裹。但是,如果 SidebarFriendsPhotos 需要載入一些數據,React 將發送 BigSpinner 後備的 HTML。然後,隨著更多數據可用,將繼續顯示更多內容,直到所有內容都可見為止。

串流不需要等待 React 本身在瀏覽器中載入,也不需要等待您的應用程式變得具有互動性。來自伺服器的 HTML 內容會在任何 <script> 標籤載入之前逐步顯示。

閱讀更多關於 HTML 串流如何運作的資訊。

注意事項

只有啟用 Suspense 的資料來源才會啟動 Suspense 組件。它們包括:

  • 使用支援 Suspense 的框架(例如 RelayNext.js)來擷取資料
  • 使用 lazy 進行元件的延遲載入
  • 使用 use 讀取 Promise 的值

Suspense 不會偵測在 Effect 或事件處理函式內擷取資料的動作。

在上述 Posts 元件中載入資料的確切方式取決於您使用的框架。如果您使用支援 Suspense 的框架,您可以在其資料擷取文件中找到詳細資訊。

目前不支援在不使用特定框架的情況下進行 Suspense 的資料擷取。實作支援 Suspense 的資料來源的需求並不穩定且缺乏文件。在未來的 React 版本中將會發布官方 API,用於將資料來源與 Suspense 整合。


指定 Shell 的內容

應用程式中任何 <Suspense> 邊界以外的部分稱為 *Shell*:

function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<BigSpinner />}>
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</Suspense>
</ProfileLayout>
);
}

它決定了使用者可能看到的最初載入狀態

<ProfileLayout>
<ProfileCover />
<BigSpinner />
</ProfileLayout>

如果您將整個應用程式包裝在根目錄的 <Suspense> 邊界中,Shell 將只包含該載入圖示。然而,這並不是一個良好的使用者體驗,因為在螢幕上看到一個大型載入圖示會比多等待一點時間並看到實際佈局感覺更慢且更煩人。這就是為什麼您通常會希望放置 <Suspense> 邊界,使 Shell 感覺*簡潔但完整*—就像整個頁面佈局的骨架一樣。

當整個 Shell 完成渲染時,onShellReady 回呼函式會觸發。通常,您會在那時候開始串流傳輸

const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
}
});

onShellReady 觸發時,巢狀 <Suspense> 邊界中的元件可能仍在載入資料。


記錄伺服器上的錯誤

預設情況下,伺服器上的所有錯誤都會記錄到主控台。您可以覆寫此行為來記錄錯誤報告

const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
},
onError(error) {
console.error(error);
logServerCrashReport(error);
}
});

如果您提供自定義的 onError 實作,請不要忘記也要像上面一樣將錯誤記錄到主控台。


從 Shell 內的錯誤中復原

在此範例中,Shell 包含 ProfileLayoutProfileCoverPostsGlimmer

function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}

如果在渲染這些元件時發生錯誤,React 將沒有任何有意義的 HTML 可以發送到客戶端。覆寫 onShellError 以發送不依賴伺服器渲染的後備 HTML 作為最後手段

const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
},
onShellError(error) {
response.statusCode = 500;
response.setHeader('content-type', 'text/html');
response.send('<h1>Something went wrong</h1>');
},
onError(error) {
console.error(error);
logServerCrashReport(error);
}
});

如果在產生 Shell 時發生錯誤,onErroronShellError 都會觸發。使用 onError 進行錯誤報告,並使用 onShellError 發送後備 HTML 文件。您的後備 HTML 不一定是錯誤頁面。相反,您可以包含一個替代 Shell,僅在客戶端上渲染您的應用程式。


從 Shell 外的錯誤中復原

在此範例中,<Posts /> 元件被包裝在 <Suspense> 中,因此它*不是* Shell 的一部分

function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}

如果在 Posts 元件或其內部的某處發生錯誤,React 將嘗試從錯誤中復原:

  1. 它會將最近 <Suspense> 邊界(PostsGlimmer)的載入後備內容發送到 HTML 中。
  2. 它將「放棄」在伺服器上渲染 Posts 內容的嘗試。
  3. 當 JavaScript 程式碼在客戶端上載入時,React 將在客戶端上*重試*渲染 Posts

如果在客戶端上重試渲染 Posts *也*失敗,React 將在客戶端上拋出錯誤。與渲染期間拋出的所有錯誤一樣,最近的父錯誤邊界決定如何向使用者顯示錯誤。實際上,這意味著使用者將會看到載入指示器,直到確定錯誤無法復原為止。

如果在客戶端上重試渲染 Posts 成功,伺服器端的載入後備內容將會被客戶端渲染的輸出取代。使用者不會知道發生了伺服器錯誤。但是,伺服器 onError 回呼函式和客戶端 onRecoverableError 回呼函式將會觸發,以便您可以收到錯誤通知。


設定狀態碼

串流傳輸引入了一個權衡取捨。您希望盡快開始串流傳輸頁面,以便使用者可以更快地看到內容。但是,一旦開始串流傳輸,就無法再設定回應狀態碼。

透過將您的應用程式劃分為 shell(所有 <Suspense> 邊界之上)和其餘內容,您已經解決了這個問題的一部分。如果 shell 發生錯誤,您將收到 onShellError 回呼,讓您可以設定錯誤狀態碼。否則,您知道應用程式可能會在客戶端恢復,因此您可以發送「OK」。

const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.statusCode = 200;
response.setHeader('content-type', 'text/html');
pipe(response);
},
onShellError(error) {
response.statusCode = 500;
response.setHeader('content-type', 'text/html');
response.send('<h1>Something went wrong</h1>');
},
onError(error) {
console.error(error);
logServerCrashReport(error);
}
});

如果 shell *外部*的元件(即在 <Suspense> 邊界內)拋出錯誤,React 不會停止渲染。這表示 onError 回呼將會觸發,但您仍然會收到 onShellReady 而不是 onShellError。這是因為 React 會嘗試在客戶端從該錯誤中恢復,如上所述

但是,如果您願意,您可以利用發生錯誤的事實來設定狀態碼

let didError = false;

const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.statusCode = didError ? 500 : 200;
response.setHeader('content-type', 'text/html');
pipe(response);
},
onShellError(error) {
response.statusCode = 500;
response.setHeader('content-type', 'text/html');
response.send('<h1>Something went wrong</h1>');
},
onError(error) {
didError = true;
console.error(error);
logServerCrashReport(error);
}
});

這只會捕獲在產生初始 shell 內容時發生的 shell 外部的錯誤,因此它並不詳盡。如果知道某些內容是否發生錯誤至關重要,您可以將其移至 shell 中。


以不同的方式處理不同的錯誤

您可以建立自己的 Error 子類別,並使用 instanceof 運算子來檢查拋出了哪個錯誤。例如,您可以定義一個自訂的 NotFoundError 並從您的元件中拋出它。然後,您的 onErroronShellReadyonShellError 回呼可以根據錯誤類型執行不同的操作

let didError = false;
let caughtError = null;

function getStatusCode() {
if (didError) {
if (caughtError instanceof NotFoundError) {
return 404;
} else {
return 500;
}
} else {
return 200;
}
}

const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
response.statusCode = getStatusCode();
response.setHeader('content-type', 'text/html');
pipe(response);
},
onShellError(error) {
response.statusCode = getStatusCode();
response.setHeader('content-type', 'text/html');
response.send('<h1>Something went wrong</h1>');
},
onError(error) {
didError = true;
caughtError = error;
console.error(error);
logServerCrashReport(error);
}
});

請記住,一旦您發出 shell 並開始串流傳輸,就無法更改狀態碼。


等待所有內容載入以供爬蟲程式和靜態生成使用

串流傳輸提供了更好的使用者體驗,因為使用者可以在內容可用時看到內容。

但是,當爬蟲程式造訪您的頁面時,或者如果您在建置時生成頁面,您可能希望先載入所有內容,然後產生最終的 HTML 輸出,而不是逐步顯示它。

您可以使用 onAllReady 回呼等待所有內容載入

let didError = false;
let isCrawler = // ... depends on your bot detection strategy ...

const { pipe } = renderToPipeableStream(<App />, {
bootstrapScripts: ['/main.js'],
onShellReady() {
if (!isCrawler) {
response.statusCode = didError ? 500 : 200;
response.setHeader('content-type', 'text/html');
pipe(response);
}
},
onShellError(error) {
response.statusCode = 500;
response.setHeader('content-type', 'text/html');
response.send('<h1>Something went wrong</h1>');
},
onAllReady() {
if (isCrawler) {
response.statusCode = didError ? 500 : 200;
response.setHeader('content-type', 'text/html');
pipe(response);
}
},
onError(error) {
didError = true;
console.error(error);
logServerCrashReport(error);
}
});

一般訪客將獲得 progressively loaded 內容串流。爬蟲程式將在所有資料載入後收到最終的 HTML 輸出。但是,這也意味著爬蟲程式必須等待*所有*資料,其中一些資料可能載入速度緩慢或發生錯誤。根據您的應用程式,您可以選擇也將 shell 發送到爬蟲程式。


中止伺服器渲染

您可以在逾時後強制伺服器渲染「放棄」

const { pipe, abort } = renderToPipeableStream(<App />, {
// ...
});

setTimeout(() => {
abort();
}, 10000);

React 將剩餘的載入後備程式碼以 HTML 形式清除,並嘗試在客戶端上渲染其餘部分。