renderToReadableStream

renderToReadableStream 将 React 树渲染到一个可读 Web 流

const stream = await renderToReadableStream(reactNode, options?)

注意

此 API 依赖于Web Streams。对于 Node.js,请改用renderToPipeableStream


参考

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

返回的流具有一个附加属性

  • allReady:一个 Promise,当所有渲染完成后(包括 shell 和所有附加 内容)将被解析。您可以 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' },
});
}

除了根组件,你还需要提供一个bootstrap `<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) 和你的bootstrap `<script> 标签注入到生成的 HTML 流中。

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

在客户端,你的 bootstrap 脚本应该使用 `hydrateRoot` 调用来水化整个`document

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

hydrateRoot(document, <App />);

这将把事件监听器附加到服务器生成的 HTML,并使其具有交互性。

深入探讨

从构建输出中读取 CSS 和 JS 资源路径

最终的资源 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} /> 并传递你的`assetMap,其中包含资源 URL。

// 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 将首先发送加载回退的 HTML(`PostsGlimmer`),然后,当`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>` 标签加载之前逐渐显示。

阅读有关 HTML 流式传输工作原理的更多信息。

注意

只有启用 Suspense 的数据源才能激活 Suspense 组件。它们包括:

  • 使用支持 Suspense 的框架(例如 RelayNext.js)进行数据获取。
  • 使用 lazy 懒加载组件代码。
  • 使用 use 读取 Promise 的值。

Suspense 不会检测在 Effect 或事件处理程序内部获取的数据。

在上面的 Posts 组件中加载数据的确切方式取决于您的框架。如果您使用的是支持 Suspense 的框架,则可以在其数据获取文档中找到详细信息。

目前还不支持在不使用特定框架的情况下进行支持 Suspense 的数据获取。实现支持 Suspense 的数据源的要求不稳定且未记录。未来版本的 React 将发布用于将数据源与 Suspense 集成的官方 API。


指定 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 包含 ProfileLayoutProfileCoverPostsGlimmer

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 仅在客户端渲染您的应用程序。


从 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出错,您的`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' },
});
}
}

普通访问者将获得逐步加载内容的流。爬虫将在所有数据加载后接收最终的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 将将剩余的加载后备内容刷新为 HTML,并将尝试在客户端渲染其余内容。