prerender 使用 Web 流 将 React 树渲染为静态 HTML 字符串。
const {prelude, postponed} = await prerender(reactNode, options?)参考
🌐 Reference
prerender(reactNode, options?)
调用 prerender 将你的应用渲染成静态 HTML。
🌐 Call prerender to render your app to static HTML.
import { prerender } from 'react-dom/static';
async function handler(request, response) {
const {prelude} = await prerender(<App />, {
bootstrapScripts: ['/main.js']
});
return new Response(prelude, {
headers: { 'content-type': 'text/html' },
});
}在客户端,调用 hydrateRoot 以使服务器生成的 HTML 具有交互性。
🌐 On the client, call hydrateRoot to make the server-generated HTML interactive.
参数
🌐 Parameters
reactNode:你想渲染为 HTML 的 React 节点。例如,一个像<App />的 JSX 节点。它预计表示整个文档,因此 App 组件应该渲染<html>标签。- 可选
options:一个具有静态生成选项的对象。- 可选
bootstrapScriptContent:如果指定,此字符串将被放置在内联<script>标签中。 - 可选
bootstrapScripts:一个字符串 URL 数组,用于在页面上发出<script>标签。使用它来包含调用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。 - 可选
onError:每当发生服务器错误时触发的回调,无论是可恢复的还是不可恢复的。默认情况下,这只会调用console.error。如果你重写它以记录崩溃报告,请确保仍然调用console.error。你也可以在发出 shell 之前使用它来调整状态码。 - 可选
progressiveChunkSize:一个块中的字节数。了解有关默认启发式方法的更多信息。 - 可选
signal:一个中止信号,它允许你中止预渲染并在客户端渲染其余部分。
- 可选
返回
🌐 Returns
prerender 返回一个 Promise:
- 如果渲染成功,则 Promise 将解析为包含以下内容的对象:
- 如果渲染失败,Promise 将被拒绝。使用此方法输出备用 shell。
注意事项
🌐 Caveats
nonce 在预渲染时不可用。每个请求的随机数(nonce)必须是唯一的,如果你使用随机数来通过 CSP 保护你的应用,那么在预渲染中包含随机数值将是不合适且不安全的。
用法
🌐 Usage
将 React 树渲染为静态 HTML 流
🌐 Rendering a React tree to a stream of static HTML
调用 prerender 将你的 React 树渲染为静态 HTML 到 可读的网络流::
🌐 Call prerender to render your React tree to static HTML into a Readable Web Stream::
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> 标签在内的整个文档。
例如,它可能看起来像这样:
🌐 For example, it might look like this:
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>在客户端,你的引导脚本应该通过调用 hydrateRoot 来 hydrating 整个 document
🌐 On the client, your bootstrap script should hydrate the entire document with a call to hydrateRoot:
import { hydrateRoot } from 'react-dom/client';
import App from './App.js';
hydrateRoot(document, <App />);这会将事件监听器附加到静态服务器生成的 HTML 并使其具有交互性。
🌐 This will attach event listeners to the static server-generated HTML and make it interactive.
深入研究
🌐 Reading CSS and JS asset paths from the build output
最终的资源 URL(如 JavaScript 和 CSS 文件)通常在构建后会被哈希。例如,你可能不会得到 styles.css,而是 styles.123456.css。对静态资源文件名进行哈希可以保证同一资源的每次不同构建都拥有不同的文件名。这很有用,因为它可以让你安全地为静态资源启用长期缓存:具有特定名称的文件内容永远不会改变。
🌐 The final asset URLs (like JavaScript and CSS files) are often hashed after the build. For example, instead of styles.css you might end up with styles.123456.css. Hashing static asset filenames guarantees that every distinct build of the same asset will have a different filename. This is useful because it lets you safely enable long-term caching for static assets: a file with a certain name would never change content.
然而,如果你在构建之后才知道资源的 URL,就无法将它们放入源代码中。例如,像之前那样将 "/styles.css" 硬编码到 JSX 中是行不通的。为了将它们保留在源代码之外,你的根组件可以从作为 prop 传入的映射中读取真实的文件名:
🌐 However, if you don’t know the asset URLs until after the build, there’s no way for you to put them in the source code. For example, hardcoding "/styles.css" into JSX like earlier wouldn’t work. To keep them out of your source code, your root component can read the real filenames from a map passed as a 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 以及资源 URL:
🌐 On the server, render <App assetMap={assetMap} /> and pass your assetMap with the asset URLs:
// 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 给客户端:
🌐 Since your server is now rendering <App assetMap={assetMap} />, you need to render it with assetMap on the client too to avoid hydration errors. You can serialize and pass assetMap to the client like this:
// 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:
🌐 In the example above, the bootstrapScriptContent option adds an extra inline <script> tag that sets the global window.assetMap variable on the client. This lets the client code read the same assetMap:
import { hydrateRoot } from 'react-dom/client';
import App from './App.js';
hydrateRoot(document, <App assetMap={window.assetMap} />);客户端和服务器端都使用相同的 assetMap 属性渲染 App,因此不会出现 hydration 错误。
🌐 Both client and server render App with the same assetMap prop, so there are no hydration errors.
将 React 树渲染为静态 HTML 字符串
🌐 Rendering a React tree to a string of static HTML
调用 prerender 将你的应用渲染为静态 HTML 字符串:
🌐 Call prerender to render your app to a static HTML string:
import { prerender } from 'react-dom/static';
async function renderToString() {
const {prelude} = await prerender(<App />, {
bootstrapScripts: ['/main.js']
});
const reader = prelude.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,使其具有交互性。
🌐 This will produce the initial non-interactive HTML output of your React components. On the client, you will need to call hydrateRoot to hydrate that server-generated HTML and make it interactive.
等待所有数据加载
🌐 Waiting for all data to load
prerender 会在完成静态 HTML 生成并解析之前等待所有数据加载完成。例如,考虑一个显示封面、带有朋友和照片的侧边栏,以及帖子列表的个人主页:
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}想象一下,<Posts /> 需要加载一些数据,这需要一些时间。理想情况下,你希望等待这些帖子加载完成,以便它们包含在 HTML 中。为此,你可以使用 Suspense 来挂起数据,而 prerender 会在挂起的内容完成之前等待,然后再解析为静态 HTML。
🌐 Imagine that <Posts /> needs to load some data, which takes some time. Ideally, you’d want wait for the posts to finish so it’s included in the HTML. To do this, you can use Suspense to suspend on the data, and prerender will wait for the suspended content to finish before resolving to the static HTML.
中止预渲染
🌐 Aborting prerendering
你可以在超时后强制预渲染“放弃”:
🌐 You can force the prerender to “give up” after a timeout:
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 prerender(<App />, {
signal: controller.signal,
});
//...任何子节点未完成的 Suspense 边界都将包含在 fallback 状态的 prelude 中。
🌐 Any Suspense boundaries with incomplete children will be included in the prelude in the fallback state.
这可以与 resume 或 resumeAndPrerender 一起用于部分预渲染。
🌐 This can be used for partial prerendering together with resume or resumeAndPrerender.
故障排除
🌐 Troubleshooting
直到整个应用渲染完成,我的流才会启动
🌐 My stream doesn’t start until the entire app is rendered
prerender 响应会等待整个应用完成渲染,包括等待所有 Suspense 边界解析完毕,然后才会解析。它是为提前的静态站点生成 (SSG) 设计的,并且不支持在加载时流式传输更多内容。
🌐 The prerender response waits for the entire app to finish rendering, including waiting for all Suspense boundaries to resolve, before resolving. It is designed for static site generation (SSG) ahead of time and does not support streaming more content as it loads.
要在内容加载时进行流式传输,请使用像 renderToReadableStream 这样的流式服务器渲染 API。
🌐 To stream content as it loads, use a streaming server render API like renderToReadableStream.