renderToReadableStream
将 React 树渲染为 可读 Web 流。
const stream = await renderToReadableStream(reactNode, options?)
参考
renderToReadableStream(reactNode, options?)
调用 renderToReadableStream
将 React 树作为 HTML 渲染到 可读 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 节点。例如,JSX 元素,如<App />
。它应该代表整个文档,因此App
组件应该渲染<html>
标签。 -
可选
options
:包含流式传输选项的对象。- 可选
bootstrapScriptContent
:如果指定,此字符串将放置在内联<script>
标签中。 - 可选
bootstrapScripts
:要在页面上发出的<script>
标签的字符串 URL 数组。使用它来包含调用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 将解析为 可读 Web 流。
- 如果渲染 shell 失败,则 Promise 将被拒绝。使用它可以输出一个备用 shell。
返回的流有一个额外的属性
allReady
:当所有渲染完成后(包括 shell 和所有其他 内容)时,该 Promise 将解析。您可以在为爬虫和静态生成返回响应之前,await stream.allReady
。如果您这样做,您将不会获得任何渐进式加载。该流将包含最终的 HTML。
用法
将 React 树作为 HTML 渲染到可读 Web 流
调用 renderToReadableStream
将 React 树作为 HTML 渲染到 可读 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' },
});
}
除了 根组件 之外,您还需要提供一个 引导 <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
的调用来对整个 文档
进行注水:
import { hydrateRoot } from 'react-dom/client';
import App from './App.js';
hydrateRoot(document, <App />);
这会将事件监听器附加到服务器生成的 HTML 并使其具有交互性。
深入探讨
最终的资源 URL(如 JavaScript 和 CSS 文件)通常在构建后会被哈希化。例如,您最终得到的可能是 styles.123456.css
,而不是 styles.css
。对静态资源文件名进行哈希处理可确保同一资源的每个不同版本的构建都具有不同的文件名。这很有用,因为它允许您安全地为静态资源启用长期缓存:具有特定名称的文件永远不会更改内容。
但是,如果您在构建完成后才知道资源 URL,则无法将它们放入源代码中。例如,像前面那样将 "/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} />
并传递包含资源 URL 的 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 将首先发送加载回退 (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 感觉*最小但完整* - 就像整个页面布局的骨架。
对 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
成功,则来自服务器的加载后备内容将被客户端渲染输出替换。用户不会知道发生了服务器错误。但是,服务器的 onError
回调函数和客户端的 onRecoverableError
回调函数将触发,以便你可以收到有关该错误的通知。
设置状态码
流式传输引入了一种权衡。您希望尽早开始流式传输页面,以便用户可以更快地看到内容。但是,一旦开始流式传输,就无法再设置响应状态码。
通过将您的应用程序划分为外壳(所有 <Suspense>
边界之上)和其余内容,您已经解决了这个问题的一部分。如果外壳出错,您的 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' },
});
}
}
如果外壳*外部*的组件(即 <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' },
});
}
}
这只会捕获在生成初始外壳内容时发生在外壳外部的错误,因此并不详尽。如果了解某些内容是否发生错误至关重要,您可以将其移至外壳中。
以不同方式处理不同错误
您可以创建自己的 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' },
});
}
}
请记住,一旦您发出外壳并开始流式传输,就无法更改状态码。
等待爬虫和静态生成加载所有内容
流式传输提供了更好的用户体验,因为用户可以在内容可用时看到内容。
但是,当爬虫访问您的页面时,或者如果您在构建时生成页面,您可能希望先加载所有内容,然后生成最终的 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' },
});
}
}
普通访客将获得一系列逐步加载的内容。爬虫将在所有数据加载后接收最终的 HTML 输出。但是,这也意味着爬虫必须等待*所有*数据,其中一些数据可能加载缓慢或出错。根据您的应用程序,您也可以选择将外壳发送到爬虫。
中止服务器渲染
您可以强制服务器渲染在超时后“放弃”
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 会将剩余的加载回退刷新为 HTML,并尝试在客户端渲染其余部分。