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 节点。例如,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。传递'http://www.w3.org/2000/svg'用于 SVG,或传递'http://www.w3.org/1998/Math/MathML'用于 MathML。 - 可选
nonce:一个nonce字符串,用于允许script-src内容安全策略 的脚本。 - 可选
onAllReady:一个回调函数,在所有渲染完成后触发,包括 初始 shell 和所有其他 内容。您可以使用它来代替onShellReady用于爬虫和静态生成。如果您在此处开始流式传输,您将不会获得任何渐进式加载。该流将包含最终的 HTML。 - 可选
onError:一个回调函数,在发生服务器错误时触发,无论错误是 可恢复的 还是 不可恢复的。默认情况下,这只会调用console.error。如果您将其覆盖为 记录崩溃报告,请确保您仍然调用console.error。您还可以使用它在 发出 shell 之前调整状态代码。 - 可选
onShellReady:一个回调函数,在 初始 shell 渲染完成后立即触发。您可以 设置状态代码 并在此处调用pipe开始流式传输。React 将 在 shell 之后流式传输其他内容,以及内联的<script>标签,这些标签将使用内容替换 HTML 加载回退。 - 可选
onShellError:一个回调函数,如果在渲染初始 shell 时出错,则会触发。它接收错误作为参数。此时流尚未发出任何字节,并且onShellReady和onAllReady都不会被调用,因此您可以 输出一个回退 HTML shell。 - 可选
progressiveChunkSize:块中的字节数。阅读有关默认启发式算法的更多信息。
- 可选
返回
renderToPipeableStream 返回一个包含两个方法的对象
pipe将 HTML 输出到提供的 可写 Node.js 流 中。如果您想启用流式传输,请在onShellReady中调用pipe,或者在onAllReady中调用pipe用于爬虫和静态生成。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 会将 文档类型 和 引导程序 <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 并使其具有交互性。
深入探讨
最终资源 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 属性渲染 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> 标签加载之前逐步显示。
指定外壳中的内容
应用程序中任何 <Suspense> 边界之外的部分称为*外壳:*
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<BigSpinner />}>
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</Suspense>
</ProfileLayout>
);
}它决定了用户可能看到的初始加载状态
<ProfileLayout>
<ProfileCover />
<BigSpinner />
</ProfileLayout>如果你将整个应用程序包装在根目录下的 <Suspense> 边界中,则外壳将仅包含该微调器。但是,这不是一种令人愉快的用户体验,因为在屏幕上看到一个大微调器比等待更长时间并看到真实布局感觉更慢、更烦人。这就是为什么通常你需要放置 <Suspense> 边界,以便外壳感觉*最小但完整*——就像整个页面布局的骨架。
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 实现,请不要忘记像上面那样将错误也记录到控制台。
从外壳内部的错误中恢复
在此示例中,外壳包含 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);
}
});如果在生成外壳时出现错误,onError 和 onShellError 都会触发。使用 onError 进行错误报告,并使用 onShellError 发送后备 HTML 文档。你的后备 HTML 不必是错误页面。相反,你可以包含一个仅在客户端渲染应用程序的备用外壳。
从外壳外部的错误中恢复
在此示例中,<Posts /> 组件包装在 <Suspense> 中,因此它*不*是外壳的一部分
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> 边界之上)和其余内容,你已经解决了这个问题的一部分。如果外壳出错,你将获得 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);
}
});如果外壳*之外*的组件(即 <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);
}
});这只会捕获在生成初始外壳内容时发生在外壳外部的错误,因此并不全面。如果了解某些内容是否发生错误至关重要,则可以将其移至外壳中。
以不同方式处理不同的错误
你可以创建你自己的 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);
}
});请记住,一旦你发送外壳并开始流式传输,就无法更改状态码。
等待爬虫和静态生成加载所有内容
流式传输提供了更好的用户体验,因为用户可以在内容可用时立即看到它。
但是,当爬虫访问你的页面时,或者如果你在构建时生成页面,你可能希望先加载所有内容,然后生成最终的 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 输出。但是,这也意味着爬虫将不得不等待*所有*数据,其中一些数据可能加载缓慢或出错。根据你的应用程序,你可以选择也向爬虫发送外壳。
中止服务器渲染
你可以在超时后强制服务器渲染“放弃”
const { pipe, abort } = renderToPipeableStream(<App />, {
// ...
});
setTimeout(() => {
abort();
}, 10000);React 会将剩余的加载后备内容作为 HTML 刷新,并尝试在客户端渲染其余内容。