prerender 使用 Web 流 将 React 树渲染到静态 HTML 字符串。

const {prelude} = await prerender(reactNode, options?)

注意

此 API 依赖于 Web Streams。 对于 Node.js,请改用 prerenderToNodeStream


参考

prerender(reactNode, options?)

调用 prerender 将你的应用渲染到静态 HTML。

import { prerender } from 'react-dom/static';

async function handler(request) {
const {prelude} = await prerender(<App />, {
bootstrapScripts: ['/main.js']
});
return new Response(prelude, {
headers: { 'content-type': 'text/html' },
});
}

在客户端,调用 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'
    • 可选 onError:每当发生服务器错误时触发的回调函数,无论错误是否可恢复不可恢复。默认情况下,此回调函数只会调用console.error。如果您将其重写为记录崩溃报告,请确保您仍然调用console.error。您还可以使用它在发出shell之前调整状态代码
    • 可选 progressiveChunkSize:块中的字节数。阅读更多关于默认启发式算法的信息。
    • 可选 signal:一个中止信号,允许您中止服务器端渲染并在客户端渲染其余部分。

返回

prerender 返回一个Promise

  • 如果渲染成功,Promise将解析为一个包含以下内容的对象:
    • prelude:一个包含HTML的Web流。您可以使用此流分块发送响应,或者可以将整个流读取到字符串中。
  • 如果渲染失败,Promise将被拒绝。使用此方法输出备用shell。

注意

我应该何时使用prerender

静态prerender API用于静态服务器端生成 (SSG)。与renderToString不同,prerender会在所有数据加载完毕后再解析。这使得它适合于生成完整页面的静态HTML,包括需要使用Suspense获取的数据。要流式传输加载的内容,请使用流式服务器端渲染 (SSR) API,例如renderToReadableStream


用法

将React树渲染到静态HTML流

调用prerender将您的React树渲染到可读Web流中的静态HTML:

import { prerender } from 'react-dom/static';

async function handler(request) {
const {prelude} = await prerender(<App />, {
bootstrapScripts: ['/main.js']
});
return new Response(prelude, {
headers: { 'content-type': 'text/html' },
});
}

除了根组件之外,您还需要提供一个引导<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,并使其具有交互性。

深入探讨

从构建输出中读取 CSS 和 JS 资源路径

最终的资源 URL(例如 JavaScript 和 CSS 文件)在构建后通常会被哈希化。例如,您可能不会得到 styles.css,而是得到 styles.123456.css。对静态资源文件名进行哈希处理可以确保相同资源的每次不同构建都具有不同的文件名。这很有用,因为它允许您安全地启用静态资源的长期缓存:具有特定名称的文件永远不会更改内容。

但是,如果您在构建后才知道资源 URL,则无法将它们放入源代码中。例如,像之前那样将 "/styles.css" 硬编码到 JSX 中是行不通的。为了避免将它们放入源代码中,根组件可以从作为 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} /> 并传递包含资源 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'
};

async function handler(request) {
const {prelude} = await prerender(<App assetMap={assetMap} />, {
bootstrapScripts: [assetMap['/main.js']]
});
return new Response(prelude, {
headers: { 'content-type': 'text/html' },
});
}

由于您的服务器现在正在渲染 <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'
};

async function handler(request) {
const {prelude} = await prerender(<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']],
});
return new Response(prelude, {
headers: { 'content-type': 'text/html' },
});
}

在上面的示例中,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,因此不会出现水合错误。


将 React 树渲染为静态 HTML 字符串

调用 prerender 将您的应用渲染为静态 HTML 字符串。

import { prerender } from 'react-dom/static';

async function renderToString() {
const {prelude} = await prerender(<App />, {
bootstrapScripts: ['/main.js']
});

const reader = stream.getReader();
let content = '';
while (true) {
const {done, value} = await reader.read();
if (done) {
return content;
}
content += Buffer.from(value).toString('utf8');
}
}

这将生成 React 组件的初始非交互式 HTML 输出。在客户端,您需要调用 hydrateRoot水合服务器生成的 HTML 并使其具有交互性。


等待所有数据加载

prerender 在完成静态 HTML 生成和解析之前会等待所有数据加载完毕。例如,考虑一个显示封面、带有朋友和照片的侧边栏以及帖子列表的个人资料页面。

function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}

假设 <Posts /> 需要加载一些数据,这需要一些时间。理想情况下,您希望等待帖子完成,以便将其包含在 HTML 中。为此,您可以使用 Suspense 挂起数据,而 prerender 将等待挂起的內容完成,然后再解析到静态 HTML。

注意

只有启用 Suspense 的数据源才会激活 Suspense 组件。它们包括:

  • 使用启用 Suspense 的框架(如 RelayNext.js)进行数据获取
  • 使用 lazy 延迟加载组件代码
  • 使用 use 读取 Promise 的值

Suspense **不会** 检测在 Effect 或事件处理程序中获取的数据。

您在上面的 Posts 组件中加载数据的确切方式取决于您的框架。如果您使用的是启用 Suspense 的框架,您可以在其数据获取文档中找到详细信息。

目前尚不支持在不使用特定框架的情况下进行启用 Suspense 的数据获取。实现启用 Suspense 的数据源的要求不稳定且未记录。将在 React 的未来版本中发布用于将数据源与 Suspense 集成的官方 API。


故障排除

我的流直到整个应用渲染完毕才开始

prerender 响应会在整个应用完成渲染(包括等待所有 suspense 边界解析)后才解析。它设计用于提前进行静态站点生成 (SSG),不支持在加载时流式传输更多内容。

要流式传输加载中的内容,请使用流式服务器渲染 API,例如 renderToReadableStream