prerenderToNodeStream 使用 Node.js Stream. 将 React 树渲染为静态 HTML 字符串。
const {prelude, postponed} = await prerenderToNodeStream(reactNode, options?)Reference
prerenderToNodeStream(reactNode, options?)
调用 prerenderToNodeStream 将你的应用渲染为静态 HTML。
import { prerenderToNodeStream } from 'react-dom/static';
// 路由处理程序语法取决于你的后端框架
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 节点。例如,一个像<App />这样的 JSX 节点。它应当表示整个文档,因此 App 组件应该渲染<html>标签。 -
optional
options:包含静态生成选项的对象。- optional
bootstrapScriptContent:如果指定,这个字符串会被放入内联<script>标签中。 - optional
bootstrapScripts:一组字符串 URL,用于页面中输出的<script>标签。用它来包含调用hydrateRoot. 的<script>。如果你完全不想在客户端运行 React,可以省略它。 - optional
bootstrapModules:类似bootstrapScripts,但会改为输出<script type="module">。 - optional
identifierPrefix:React 用于useId. 生成的 ID 的字符串前缀。在同一页面使用多个根时,这有助于避免冲突。必须与传递给hydrateRoot. 的前缀相同。 - optional
namespaceURI:流的根 namespace URI 字符串。默认是普通 HTML。SVG 传入'http://www.w3.org/2000/svg',MathML 传入'http://www.w3.org/1998/Math/MathML'。 - optional
onError:每当发生服务端错误时触发的回调,无论该错误是可恢复的还是不可恢复的。 默认情况下,这只会调用console.error。如果你覆盖它来记录崩溃报告,请确保仍然调用console.error。你也可以在 shell 输出之前用它来调整状态码。 - optional
progressiveChunkSize:每个分块中的字节数。阅读更多关于默认启发式算法的内容。 - optional
signal:一个 abort signal,可让你中止预渲染,并在客户端渲染剩余部分。
- optional
Returns
prerenderToNodeStream 返回一个 Promise:
- 如果渲染成功,Promise 将解析为一个包含以下内容的对象:
prelude:一个 Node.js Stream. 的 HTML。你可以使用这个流分块发送响应,也可以将整个流读取为字符串。postponed:一个可 JSON 序列化的、不透明的对象。如果prerenderToNodeStream未完成,它可以传递给resumeToPipeableStream。否则为null,表示prelude已包含全部内容,无需恢复。
- 如果渲染失败,Promise 将被拒绝。可用于输出一个回退 shell。
Caveats
在预渲染时,nonce 不是可用选项。nonce 必须对每个请求唯一,如果你使用 nonce 通过 CSP 保护应用,那么在预渲染结果中包含 nonce 值是不合适且不安全的。
Usage
将 React 树渲染为静态 HTML 流
调用 prerenderToNodeStream 将你的 React 树渲染为 Node.js Stream 中的静态 HTML:
import { prerenderToNodeStream } from 'react-dom/static';
// 路由处理程序语法取决于你的后端框架
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>我的应用</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 传入的 map 中读取真实文件名:
export default function App({ assetMap }) {
return (
<html>
<head>
<title>我的应用</title>
<link rel="stylesheet" href={assetMap['styles.css']}></link>
</head>
...
</html>
);
}在服务端,渲染 <App assetMap={assetMap} />,并将你的 assetMap 和资源 URL 一起传入:
// 你需要从构建工具中获取这份 JSON,例如从构建输出中读取它。
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);
});由于你的服务端现在渲染的是 <App assetMap={assetMap} />,你也需要在客户端使用 assetMap 来渲染它,以避免 hydration 错误。你可以像这样序列化并将 assetMap 传给客户端:
// 你需要从构建工具中获取这份 JSON。
const assetMap = {
'styles.css': '/styles.123456.css',
'main.js': '/main.123456.js'
};
app.use('/', async (request, response) => {
const { prelude } = await prerenderToNodeStream(<App />, {
// 注意:对这个对象使用 stringify() 是安全的,因为这些数据不是用户生成的。
bootstrapScriptContent: `window.assetMap = ${JSON.stringify(assetMap)};`,
bootstrapScripts: [assetMap['/main.js']],
});
response.setHeader('Content-Type', 'text/html');
prelude.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,因此不会出现 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);
});
}这将生成你的 React 组件初始的、非交互式的 HTML 输出。在客户端,你需要调用 hydrateRoot 来注水该服务端生成的 HTML,并使其具备交互性。
等待所有数据加载完成
prerenderToNodeStream 会在完成静态 HTML 生成并解析之前等待所有数据加载完成。例如,考虑一个展示封面、带有好友和照片的侧边栏,以及帖子列表的个人资料页面:
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}设想 <Posts /> 需要加载一些数据,这会花费一些时间。理想情况下,你会希望等待帖子加载完成,使其包含在 HTML 中。为此,你可以使用 Suspense 在数据上挂起,而 prerenderToNodeStream 会在解析为静态 HTML 之前等待挂起的内容完成。
中止预渲染
你可以强制预渲染在超时后“放弃”:
async function renderToString() {
const controller = new AbortController();
setTimeout(() => {
controller.abort()
}, 10000);
try {
// prelude 将包含在控制器中止之前
// 已预渲染的所有 HTML。
const {prelude} = await prerenderToNodeStream(<App />, {
signal: controller.signal,
});
//...任何子元素不完整的 Suspense 边界都会以回退状态包含在 prelude 中。
这可与 resumeToPipeableStream 或 resumeAndPrerenderToNodeStream 一起用于部分预渲染。
故障排除
我的流直到整个应用渲染完成后才开始
prerenderToNodeStream 响应会等待整个应用完成渲染,包括等待所有 Suspense 边界解析完成,然后才会进行解析。它是为提前进行静态站点生成(SSG)而设计的,不支持在内容加载时继续流式传输更多内容。
要在内容加载时进行流式传输,请使用类似 renderToPipeableStream 的流式服务器渲染 API。