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。对于 SVG,请传递 'http://www.w3.org/2000/svg';对于 MathML,请传递 'http://www.w3.org/1998/Math/MathML'
    • 可选 nonce:一个 nonce 字符串,允许针对 script-src 内容安全策略 的脚本。
    • 可选 onAllReady:所有渲染完成后(包括 Shell 和所有附加的 内容)触发的回调函数。您可以使用它来代替 爬虫和静态生成onShellReady。如果您在此处开始流式传输,则不会获得任何渐进式加载。流将包含最终的 HTML。
    • 可选 onError:无论 是否可恢复,只要发生服务器错误,就会触发此回调函数。默认情况下,这只会调用 console.error。如果您将其覆盖为 记录崩溃报告,请确保您仍然调用 console.error。您还可以使用它来 调整状态代码(在发出 Shell 之前)。
    • 可选 onShellReady:渲染 初始 Shell 后立即触发的回调函数。您可以在此处 设置状态代码 并调用 pipe 以开始流式传输。React 将在 Shell 之后以及内联 <script> 标签(使用内容替换 HTML 加载回退)一起 流式传输附加内容
    • 可选 onShellError:如果渲染初始 Shell 时出错,则会触发此回调函数。它接收错误作为参数。流尚未发出任何字节,并且 onShellReadyonAllReady 也不会被调用,因此您可以 输出备用 HTML Shell
    • 可选 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);
}
});
});

除了根组件,你还需要提供一个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>
...
<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'
};

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 将首先发送加载后备内容的 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 感觉最小但完整——就像整个页面布局的骨架一样。

onShellReady 回调在整个 shell 呈现后触发。通常,您会在那时开始流式传输。

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 并从您的组件中抛出它。然后您的 `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);
}
});

普通访问者将获得逐步加载内容的流。爬虫将在所有数据加载后接收最终的 HTML 输出。但是,这也意味着爬虫必须等待*所有*数据,其中一些数据加载速度可能很慢或出错。根据您的应用程序,您也可以选择将 shell 发送给爬虫。


中止服务器端渲染

您可以强制服务器端渲染在超时后“放弃”。

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

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

React 将将剩余的加载回退内容作为 HTML 输出,并将尝试在客户端渲染其余部分。