renderToReadableStream
將 React 樹狀結構渲染成 Readable Web Stream (可讀取的 Web 串流)。
const stream = await renderToReadableStream(reactNode, options?)
參考
renderToReadableStream(reactNode, options?)
呼叫 renderToReadableStream
將 React 樹狀結構以 HTML 形式渲染成 Readable Web Stream (可讀取的 Web 串流)。
import { renderToReadableStream } from 'react-dom/server';
async function handler(request) {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js']
});
return new Response(stream, {
headers: { 'content-type': 'text/html' },
});
}
在客戶端上,呼叫 hydrateRoot
使伺服器生成的 HTML 互動。
參數
-
reactNode
:您想要渲染成 HTML 的 React 節點。例如,像<App />
這樣的 JSX 元素。它應該代表整個文件,因此App
元件應該渲染<html>
標籤。 -
**選用**
options
:具有串流選項的物件。- **選用**
bootstrapScriptContent
:如果指定,此字串將被放置在一個內聯的<script>
標籤中。 - (選用)
bootstrapScripts
:一個字串 URL 的陣列,用於在頁面上輸出的<script>
標籤。使用它來包含調用hydrateRoot
的<script>
。如果您根本不想在客戶端運行 React,請省略它。 - (選用)
bootstrapModules
:與bootstrapScripts
相似,但會輸出<script type="module">
。 - (選用)
identifierPrefix
:React 用於useId
生成的 ID 的字串前綴。在同一個頁面上使用多個根目錄時,這有助於避免衝突。必須與傳遞給hydrateRoot
的前綴相同。 - (選用)
namespaceURI
:一個包含串流根 命名空間 URI 的字串。預設為一般 HTML。對於 SVG,請傳遞'http://www.w3.org/2000/svg'
;對於 MathML,請傳遞'http://www.w3.org/1998/Math/MathML'
。 - (選用)
nonce
:一個nonce
字串,允許script-src
內容安全策略 的腳本。 - (選用)
onError
:一個在發生伺服器錯誤時觸發的回調函式,無論是 可恢復 的還是 不可恢復 的。預設情況下,這只會調用console.error
。如果您覆蓋它以 記錄崩潰報告,請確保您仍然調用console.error
。您也可以使用它在發出 shell 之前 調整狀態碼。 - (選用)
progressiveChunkSize
:區塊中的位元組數。閱讀更多關於預設啟發式方法的資訊。 - (選用)
signal
:一個 中止信號,讓您可以 中止伺服器渲染 並在客戶端渲染其餘部分。
- **選用**
回傳
renderToReadableStream
會回傳一個 Promise
- 如果渲染 shell 成功,該 Promise 將會解析為一個 Readable Web Stream。
- 如果渲染 shell 失敗,Promise 將會被拒絕。使用它來輸出一個備用 shell。
回傳的串流具有一個額外的屬性
allReady
:一個 Promise,當所有渲染完成時(包括 shell 和所有額外的 內容) resolves。您可以在 爬蟲和靜態生成 回傳響應之前await stream.allReady
。如果您這樣做,您將不會獲得任何漸進式加載。串流將包含最終的 HTML。
用法
將 React 樹狀結構以 HTML 形式渲染到 Readable Web Stream
調用 renderToReadableStream
將您的 React 樹狀結構以 HTML 形式渲染到 Readable Web Stream:
import { renderToReadableStream } from 'react-dom/server';
async function handler(request) {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js']
});
return new Response(stream, {
headers: { 'content-type': 'text/html' },
});
}
除了根組件之外,您還需要提供一個啟動用的 <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 會將 文件類型 和您的 啟動用的 <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>
<title>My app</title>
<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'
};
async function handler(request) {
const stream = await renderToReadableStream(<App assetMap={assetMap} />, {
bootstrapScripts: [assetMap['/main.js']]
});
return new Response(stream, {
headers: { 'content-type': 'text/html' },
});
}
由於您的伺服器現在正在渲染 <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'
};
async function handler(request) {
const stream = await renderToReadableStream(<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']],
});
return new Response(stream, {
headers: { 'content-type': 'text/html' },
});
}
在上面的範例中,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 將首先發送載入 fallback(PostsGlimmer
)的 HTML,然後,當 Posts
完成數據載入後,React 將發送剩餘的 HTML 以及一個內聯 <script>
標籤,該標籤會將載入 fallback 替換為該 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
fallback 的 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 感覺 *簡潔但完整*——就像整個頁面佈局的骨架。
對 `renderToReadableStream
` 的非同步呼叫將在整個 shell 完成渲染後立即解析為 `stream
`。通常,您將通過使用該 `stream
` 建立和返回響應來開始串流。
async function handler(request) {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js']
});
return new Response(stream, {
headers: { 'content-type': 'text/html' },
});
}
在返回 `stream
` 時,巢狀 `<Suspense>
` 邊界中的元件可能仍在載入資料。
記錄伺服器端的錯誤
預設情況下,伺服器上的所有錯誤都會記錄到主控台。您可以覆蓋此行為以記錄錯誤報告
async function handler(request) {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js'],
onError(error) {
console.error(error);
logServerCrashReport(error);
}
});
return new Response(stream, {
headers: { 'content-type': 'text/html' },
});
}
如果您提供自定義的 `onError
` 實作,請不要忘記也將錯誤記錄到如上所示的主控台。
從 shell 內的錯誤中恢復
在此範例中,shell 包含 `ProfileLayout
`、`ProfileCover
` 和 `PostsGlimmer
`
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}
如果在渲染這些元件時發生錯誤,React 將沒有任何有意義的 HTML 可以發送到客戶端。將您的 `renderToReadableStream
` 呼叫包裝在 `try...catch
` 中,以便在最後手段發送不依賴伺服器渲染的後備 HTML
async function handler(request) {
try {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js'],
onError(error) {
console.error(error);
logServerCrashReport(error);
}
});
return new Response(stream, {
headers: { 'content-type': 'text/html' },
});
} catch (error) {
return new Response('<h1>Something went wrong</h1>', {
status: 500,
headers: { 'content-type': 'text/html' },
});
}
}
如果在產生 shell 時發生錯誤,`onError
` 和您的 `catch
` 區塊都會觸發。使用 `onError
` 進行錯誤報告,並使用 `catch
` 區塊發送後備 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
成功,伺服器端的載入 fallback 會被客戶端渲染的輸出取代。使用者不會知道發生了伺服器錯誤。然而,伺服器端的 onError
回呼函式和客戶端端的 onRecoverableError
回呼函式會觸發,以便您收到錯誤通知。
設定狀態碼
串流傳輸引入了一種權衡。您希望盡快開始串流頁面,以便使用者可以更快地看到內容。但是,一旦開始串流傳輸,就無法再設定回應狀態碼。
透過將您的應用程式劃分為 shell(所有 <Suspense>
邊界之上)和其他內容,您已經解決了這個問題的一部分。如果 shell 發生錯誤,您的 catch
區塊將會運行,讓您可以設定錯誤狀態碼。否則,您就知道應用程式可能會在客戶端恢復,因此您可以發送「OK」。
async function handler(request) {
try {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js'],
onError(error) {
console.error(error);
logServerCrashReport(error);
}
});
return new Response(stream, {
status: 200,
headers: { 'content-type': 'text/html' },
});
} catch (error) {
return new Response('<h1>Something went wrong</h1>', {
status: 500,
headers: { 'content-type': 'text/html' },
});
}
}
如果 shell *外部* 的組件(即在 <Suspense>
邊界內)拋出錯誤,React 不會停止渲染。這表示 onError
回呼函式會觸發,但您的程式碼會繼續運行,而不會進入 catch
區塊。這是因為 React 會嘗試在客戶端從該錯誤中恢復,如上所述。
但是,如果您願意,您可以利用發生錯誤的事實來設定狀態碼
async function handler(request) {
try {
let didError = false;
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js'],
onError(error) {
didError = true;
console.error(error);
logServerCrashReport(error);
}
});
return new Response(stream, {
status: didError ? 500 : 200,
headers: { 'content-type': 'text/html' },
});
} catch (error) {
return new Response('<h1>Something went wrong</h1>', {
status: 500,
headers: { 'content-type': 'text/html' },
});
}
}
這只會捕捉在產生初始 shell 內容時發生的 shell 外部錯誤,因此它並不詳盡。如果知道某些內容是否發生錯誤至關重要,您可以將其移至 shell 中。
以不同方式處理不同的錯誤
您可以建立自己的 Error
子類別,並使用 instanceof
運算子來檢查拋出了哪個錯誤。例如,您可以定義一個自訂的 NotFoundError
並從您的組件中拋出它。然後您可以將錯誤儲存在 onError
中,並根據錯誤類型在返回回應之前執行不同的操作
async function handler(request) {
let didError = false;
let caughtError = null;
function getStatusCode() {
if (didError) {
if (caughtError instanceof NotFoundError) {
return 404;
} else {
return 500;
}
} else {
return 200;
}
}
try {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js'],
onError(error) {
didError = true;
caughtError = error;
console.error(error);
logServerCrashReport(error);
}
});
return new Response(stream, {
status: getStatusCode(),
headers: { 'content-type': 'text/html' },
});
} catch (error) {
return new Response('<h1>Something went wrong</h1>', {
status: getStatusCode(),
headers: { 'content-type': 'text/html' },
});
}
}
請記住,一旦您發出 shell 並開始串流傳輸,就無法更改狀態碼。
等待爬蟲程式和靜態生成載入所有內容
串流傳輸提供了更好的使用者體驗,因為使用者可以看到內容變得可用。
但是,當爬蟲程式訪問您的頁面時,或者如果您在建置時生成頁面,您可能希望先載入所有內容,然後產生最終的 HTML 輸出,而不是逐步顯示它。
您可以透過等待 stream.allReady
Promise 來等待所有內容載入
async function handler(request) {
try {
let didError = false;
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js'],
onError(error) {
didError = true;
console.error(error);
logServerCrashReport(error);
}
});
let isCrawler = // ... depends on your bot detection strategy ...
if (isCrawler) {
await stream.allReady;
}
return new Response(stream, {
status: didError ? 500 : 200,
headers: { 'content-type': 'text/html' },
});
} catch (error) {
return new Response('<h1>Something went wrong</h1>', {
status: 500,
headers: { 'content-type': 'text/html' },
});
}
}
一般訪客將獲得 progressively loaded 的內容串流。爬蟲程式將在所有資料載入後收到最終的 HTML 輸出。然而,這也意味著爬蟲程式必須等待*所有*資料,其中一些資料可能載入緩慢或發生錯誤。根據您的應用程式,您可以選擇也將 shell 發送到爬蟲程式。
中止伺服器渲染
您可以在逾時後強制伺服器渲染「放棄」
async function handler(request) {
try {
const controller = new AbortController();
setTimeout(() => {
controller.abort();
}, 10000);
const stream = await renderToReadableStream(<App />, {
signal: controller.signal,
bootstrapScripts: ['/main.js'],
onError(error) {
didError = true;
console.error(error);
logServerCrashReport(error);
}
});
// ...
React 將剩餘的載入 fallback 刷新為 HTML,並嘗試在客戶端渲染其餘部分。