renderToPipeableStream 将一个 React 树渲染到可管道传输的 Node.js Stream。
const { pipe, abort } = renderToPipeableStream(reactNode, options?)参考
renderToPipeableStream(reactNode, options?)
调用 renderToPipeableStream,将你的 React 树渲染为 HTML,并输出到 Node.js Stream。
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:流的根 namespace URI 字符串。默认是普通 HTML。SVG 传'http://www.w3.org/2000/svg',MathML 传'http://www.w3.org/1998/Math/MathML'。 - 可选
nonce:一个nonce字符串,用于允许script-srcContent-Security-Policy 下的脚本。 - 可选
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 Stream。 如果你想启用流式传输,就在onShellReady中调用pipe;如果是爬虫和静态生成,则在onAllReady中调用。abort允许你 中止服务端渲染 并在客户端渲染剩余部分。
用法
将 React 树渲染为 Node.js Stream 的 HTML
调用 renderToPipeableStream,将你的 React 树渲染为 HTML 并输出到 Node.js Stream:
import { renderToPipeableStream } from 'react-dom/server';
// 路由处理器语法取决于你的后端框架
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>我的应用</title>
</head>
<body>
<Router />
</body>
</html>
);
}React 会将 doctype 和你的 bootstrap <script> 标签 注入到生成的 HTML 流中:
<!DOCTYPE html>
<html>
<!-- ... 来自你的组件的 HTML ... -->
</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 上,并使其可交互。
Deep Dive
最终的资源 URL(如 JavaScript 和 CSS 文件)通常会在构建后被哈希化。例如,你可能最终得到的不是 styles.css,而是 styles.123456.css。对静态资源文件名进行哈希处理,可以保证同一资源的每个不同构建都有不同的文件名。这很有用,因为它允许你安全地为静态资源启用长期缓存:某个名称的文件内容将永远不会改变。
但是,如果你在构建完成之前不知道资源 URL,就无法把它们写进源代码。比如,像前面那样在 JSX 中硬编码 "/styles.css" 就不会工作。为了把它们排除在源代码之外,你的根组件可以从作为 prop 传入的映射中读取真实文件名:
export default function App({ assetMap }) {
return (
<html>
<head>
...
<link rel="stylesheet" href={assetMap['styles.css']}></link>
...
</head>
...
</html>
);
}在服务端,渲染 <App assetMap={assetMap} />,并把包含资源 URL 的 assetMap 传入:
// 你需要从构建工具中获取这个 JSON,例如从构建输出中读取它。
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 传给客户端:
// 你需要从构建工具中获取这个 JSON。
const assetMap = {
'styles.css': '/styles.123456.css',
'main.js': '/main.123456.js'
};
app.use('/', (request, response) => {
const { pipe } = renderToPipeableStream(<App assetMap={assetMap} />, {
// 注意:这样 stringify() 是安全的,因为这些数据不是用户生成的。
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 会先发送加载占位(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 感觉尽可能精简但完整——就像整个页面布局的骨架一样。
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 包含 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>出错了</h1>');
},
onError(error) {
console.error(error);
logServerCrashReport(error);
}
});如果在生成 shell 时出错,onError 和 onShellError 都会触发。使用 onError 进行错误上报,使用 onShellError 发送备用 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 回调都会触发,这样你就能收到错误通知。
设置状态码
流式传输带来了一种权衡。你希望尽早开始流式输出页面,这样用户就能更早看到内容。然而,一旦开始流式传输,你就不能再设置响应状态码了。
通过将你的应用划分为 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>出错了</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>出错了</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>出错了</h1>');
},
onError(error) {
didError = true;
caughtError = error;
console.error(error);
logServerCrashReport(error);
}
});请记住,一旦你输出了 shell 并开始流式传输,就不能再更改状态码了。
等待所有内容加载完毕,供爬虫和静态生成使用
流式传输能提供更好的用户体验,因为用户可以在内容可用时立即看到它。
不过,当爬虫访问你的页面,或者你在构建时生成页面时,你可能会希望先让所有内容都加载完,然后再输出最终 HTML,而不是逐步揭示它。
你可以使用 onAllReady 回调等待所有内容加载完毕:
let didError = false;
let isCrawler = // ... 取决于你的机器人检测策略 ...
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>出错了</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 刷出,并尝试在客户端渲染其余部分。