renderToPipeableStream
將 React 樹狀結構渲染成可管道化的 Node.js 串流。
const { pipe, abort } = renderToPipeableStream(reactNode, options?)
參考
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
:如果渲染初始框架時發生錯誤,則會觸發的回調函式。它會接收錯誤作為參數。此時串流尚未發出任何位元組,且onShellReady
和onAllReady
都將不會被呼叫,因此您可以 輸出後備 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 並使其具有互動性。
深入探討
最終的資源網址(例如 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 可以更早地開始串流頁面。只有 ProfileLayout
和 ProfileCover
必須先完成渲染,因為它們沒有被任何 <Suspense>
邊界包裹。但是,如果 Sidebar
、Friends
或 Photos
需要載入一些數據,React 將發送 BigSpinner
後備的 HTML。然後,隨著更多數據可用,將繼續顯示更多內容,直到所有內容都可見為止。
串流不需要等待 React 本身在瀏覽器中載入,也不需要等待您的應用程式變得具有互動性。來自伺服器的 HTML 內容會在任何 <script>
標籤載入之前逐步顯示。
指定 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 包含 ProfileLayout
、ProfileCover
和 PostsGlimmer
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 時發生錯誤,onError
和 onShellError
都會觸發。使用 onError
進行錯誤報告,並使用 onShellError
發送後備 HTML 文件。您的後備 HTML 不一定是錯誤頁面。相反,您可以包含一個替代 Shell,僅在客戶端上渲染您的應用程式。
從 Shell 外的錯誤中復原
在此範例中,<Posts />
元件被包裝在 <Suspense>
中,因此它*不是* Shell 的一部分
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}
如果在 Posts
元件或其內部的某處發生錯誤,React 將嘗試從錯誤中復原:
- 它會將最近
<Suspense>
邊界(PostsGlimmer
)的載入後備內容發送到 HTML 中。 - 它將「放棄」在伺服器上渲染
Posts
內容的嘗試。 - 當 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
並從您的元件中拋出它。然後,您的 onError
、onShellReady
和 onShellError
回呼可以根據錯誤類型執行不同的操作
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 形式清除,並嘗試在客戶端上渲染其餘部分。