prerenderToNodeStream
prerenderToNodeStream
使用 Node.js Stream. 将 React 树渲染为静态 HTML 字符串。
const {prelude} = await prerenderToNodeStream(reactNode, options?)
参考
prerenderToNodeStream(reactNode, options?)
调用 prerenderToNodeStream
将应用渲染为静态 HTML。
import { prerenderToNodeStream } from 'react-dom/static';
// The route handler syntax depends on your backend framework
app.use('/', async (request, response) => {
const { prelude } = await prerenderToNodeStream(<App />, {
bootstrapScripts: ['/main.js'],
});
response.setHeader('Content-Type', 'text/plain');
prelude.pipe(response);
});
在客户端,使用 hydrateRoot
将服务器生成的 HTML 变为可交互。
Parameters
-
reactNode
:要渲染为 HTML 的 React 节点。例如 JSX 节点<App />
。它应代表整个文档,因此App
组件应渲染<html>
标签。 -
可选
options
:一个用于静态生成的选项对象。- 可选
bootstrapScriptContent
:若指定,该字符串会放入内联<script>
标签中。 - 可选
bootstrapScripts
:要在页面中输出的<script>
标签 URL 字符串数组。用于包含调用hydrateRoot
的脚本;如果不希望在客户端运行 React,可省略此项。 - 可选
bootstrapModules
:与bootstrapScripts
类似,但输出的是<script type="module">
。 - 可选
identifierPrefix
:React 用于useId
生成 ID 的字符串前缀。当页面上存在多个 root 时可避免冲突。此值必须与传给hydrateRoot
的前缀相同。 - 可选
namespaceURI
:流的根 namespace URI 字符串。默认是普通 HTML。若为 SVG,请传'http://www.w3.org/2000/svg'
;若为 MathML,请传'http://www.w3.org/1998/Math/MathML'
。 - 可选
onError
:当服务器发生错误(可恢复或不可恢复)时触发的回调。默认仅会调用console.error
。如果你重写它以记录崩溃报告,请确保仍然调用console.error
。你也可以在 shell 发出之前使用它来调整响应状态码。 - 可选
progressiveChunkSize
:每个 chunk 的字节数。 了解默认启发式的更多信息。 - 可选
signal
:一个 abort signal,可以用来 中止 prerender,并在客户端渲染剩余部分。
- 可选
Returns
prerenderToNodeStream
返回一个 Promise:
- 如果渲染成功,该 Promise 会解析为一个对象,包含:
prelude
:用于 HTML 的 Node.js Stream。你可以使用这个流按块(chunk)发送响应,也可以将整个流读取为一个字符串。
- 如果渲染失败,该 Promise 将被拒绝。请参阅 使用此方法输出 fallback(占位 UI)外壳,了解如何在出错时提供占位页面。
注意事项
在 prerender 时无法使用 nonce
选项。nonce
必须对每次请求保持唯一;如果你使用 nonce 配合 CSP 来保护应用,那么在 prerender 的输出中包含该 nonce 值是不合适且不安全的。
用法
将 React 树渲染到静态 HTML 的流中
调用 prerenderToNodeStream
可将 React 树渲染为指向 Node.js Stream 的静态 HTML:
import { prerenderToNodeStream } from 'react-dom/static';
// The route handler syntax depends on your backend framework
app.use('/', async (request, response) => {
const { prelude } = await prerenderToNodeStream(<App />, {
bootstrapScripts: ['/main.js'],
});
response.setHeader('Content-Type', 'text/plain');
prelude.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
做 hydration:
import { hydrateRoot } from 'react-dom/client';
import App from './App.js';
hydrateRoot(document, <App />);
这会为服务器生成的静态 HTML 附加事件监听器,使其变为可交互。
深入探讨
构建后静态资源通常会被哈希,例如 styles.css
可能变为 styles.123456.css
。哈希文件名保证每次构建的同名资源在文件内容变化时文件名也会变化,从而可以安全开启长期缓存。
如果你在构建后才能获取到最终资源名,就无法在源代码中硬编码这些路径。为此,根组件可以通过 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
:
// 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('/', async (request, response) => {
const { prelude } = await prerenderToNodeStream(<App />, {
bootstrapScripts: [assetMap['/main.js']]
});
response.setHeader('Content-Type', 'text/html');
prelude.pipe(response);
});
因为现在服务器端是用 assetMap
渲染 <App assetMap={assetMap} />
,客户端也需要以相同方式渲染以避免 hydration 错误。你可以像下面这样将 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('/', async (request, response) => {
const { prelude } = await prerenderToNodeStream(<App />, {
// Careful: It's safe to stringify() this because this data isn't user-generated.
bootstrapScriptContent: `window.assetMap = ${JSON.stringify(assetMap)};`,
bootstrapScripts: [assetMap['/main.js']],
});
response.setHeader('Content-Type', 'text/html');
prelude.pipe(response);
});
上例中 bootstrapScriptContent
会添加一个内联脚本,在客户端设置全局变量 window.assetMap
,从而让客户端代码读取相同的 assetMap
:
import { hydrateRoot } from 'react-dom/client';
import App from './App.js';
hydrateRoot(document, <App assetMap={window.assetMap} />);
服务器与客户端均以相同的 assetMap
渲染 App
,因此不会出现 hydration 错误。
将 React 树渲染为静态 HTML 字符串
调用 prerenderToNodeStream
将应用渲染为静态 HTML 字符串:
import { prerenderToNodeStream } from 'react-dom/static';
async function renderToString() {
const {prelude} = await prerenderToNodeStream(<App />, {
bootstrapScripts: ['/main.js']
});
return new Promise((resolve, reject) => {
let data = '';
prelude.on('data', chunk => {
data += chunk;
});
prelude.on('end', () => resolve(data));
prelude.on('error', reject);
});
}
这会产生组件的初始非交互式 HTML 输出。在客户端,你需要调用 hydrateRoot
来 hydrate 该服务器生成的 HTML,使其变为可交互。
等待所有数据加载完成
prerenderToNodeStream
会等待所有数据加载完成后再结束静态 HTML 的生成并 resolve。例如,考虑包含封面、侧边栏(好友与照片)和帖子列表的个人资料页:
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}
假设 <Posts />
需要加载数据且耗时较长。若你希望在静态 HTML 中包含这些帖子内容,可以使用 Suspense 挂起数据,prerenderToNodeStream
会等待挂起内容完成后再将其包含在生成的静态 HTML 内。
中止 prerender(aborting-prerendering)
你可以在超时后强制 prerender “放弃”:
async function renderToString() {
const controller = new AbortController();
setTimeout(() => {
controller.abort()
}, 10000);
try {
// the prelude will contain all the HTML that was prerendered
// before the controller aborted.
const {prelude} = await prerenderToNodeStream(<App />, {
signal: controller.signal,
});
//...
任何仍未完成的 Suspense 边界会以 fallback 状态包含在 prelude 中。
Troubleshooting
当整个应用渲染完成之前流没有开始输出怎么办?
prerenderToNodeStream
会等待整个应用渲染完成(包括所有 Suspense 边界)后再 resolve。它的设计目的是用于静态站点生成(SSG),不支持在内容加载时逐步流式输出更多内容。
若想在内容加载过程中就开始流式输出,请使用流式 SSR API,例如 renderToPipeableStream。