renderToReadableStream 将 React 树渲染为 可读的 Web 流。
const stream = await renderToReadableStream(reactNode, options?)参考
🌐 Reference
renderToReadableStream(reactNode, options?)
调用 renderToReadableStream 将你的 React 树渲染为 HTML 到 可读 Web 流
🌐 Call renderToReadableStream to render your React tree as HTML into a Readable Web Stream.
import { renderToReadableStream } from 'react-dom/server';
async function handler(request) {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js']
});
return new Response(stream, {
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。 - 可选
nonce:一个nonce字符串,用于允许script-src内容安全策略`的脚本。 - 可选
onError:每当发生服务器错误时触发的回调,无论是可恢复的还是不可恢复的。默认情况下,这只会调用console.error。如果你重写它以记录崩溃报告,请确保仍然调用console.error。你也可以在发出 shell 之前使用它来调整状态码。 - 可选
progressiveChunkSize:一个块中的字节数。了解有关默认启发式方法的更多信息。 - 可选
signal:一个 中止信号,允许你 中止服务器渲染 并在客户端渲染剩余的内容。
- 可选
返回
🌐 Returns
renderToReadableStream 返回一个 Promise:
- 如果渲染 shell 成功,该 Promise 将解析为一个 可读的网络流。
- 如果渲染外壳失败,Promise 将被拒绝。使用此方法输出备用外壳。
返回的流有一个额外的属性:
🌐 The returned stream has an additional property:
allReady:一个 Promise,当所有渲染完成时会被解决,包括 shell 和所有额外的 内容. 你可以在返回响应之前await stream.allReady用于爬虫和静态生成. 如果你这样做,你将不会得到任何渐进式加载。流将包含最终的 HTML。
用法
🌐 Usage
将 React 树作为 HTML 渲染到可读的 Web 流
🌐 Rendering a React tree as HTML to a Readable Web Stream
调用 renderToReadableStream 将你的 React 树渲染为 HTML 到 可读 Web 流:
🌐 Call renderToReadableStream to render your React tree as HTML into a Readable Web Stream:
import { renderToReadableStream } from 'react-dom/server';
async function handler(request) {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js']
});
return new Response(stream, {
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 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 stream = await renderToReadableStream(<App assetMap={assetMap} />, {
bootstrapScripts: [assetMap['/main.js']]
});
return new Response(stream, {
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 stream = await renderToReadableStream(<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(stream, {
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.
在加载时流式传输更多内容
🌐 Streaming more content as it loads
流媒体允许用户在服务器上所有数据完全加载之前就开始查看内容。例如,考虑一个显示封面、带有朋友和照片侧边栏以及帖子列表的个人资料页面:
🌐 Streaming allows the user to start seeing the content even before all the data has loaded on the server. For example, consider a profile page that shows a cover, a sidebar with friends and photos, and a list of posts:
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Posts />
</ProfileLayout>
);
}想象一下,为 <Posts /> 加载数据需要一些时间。理想情况下,你希望在不等待帖子加载的情况下向用户显示个人资料页面的其余内容。为此,将 Posts 封装在 <Suspense> 边界中:
🌐 Imagine that loading data for <Posts /> takes some time. Ideally, you’d want to show the rest of the profile page content to the user without waiting for the posts. To do this, wrap Posts in a <Suspense> boundary:
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 替换。
🌐 This tells React to start streaming the HTML before Posts loads its data. React will send the HTML for the loading fallback (PostsGlimmer) first, and then, when Posts finishes loading its data, React will send the remaining HTML along with an inline <script> tag that replaces the loading fallback with that HTML. From the user’s perspective, the page will first appear with the PostsGlimmer, later replaced by the Posts.
你可以进一步嵌套 <Suspense> 边界来创建更细粒度的加载顺序:
🌐 You can further nest <Suspense> boundaries to create a more granular loading sequence:
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。然后,随着更多数据的可用,更多内容将会继续显现,直到全部内容可见。
🌐 In this example, React can start streaming the page even earlier. Only ProfileLayout and ProfileCover must finish rendering first because they are not wrapped in any <Suspense> boundary. However, if Sidebar, Friends, or Photos need to load some data, React will send the HTML for the BigSpinner fallback instead. Then, as more data becomes available, more content will continue to be revealed until all of it becomes visible.
流式传输不需要等待 React 本身在浏览器中加载,也不需要等你的应用变得可交互。来自服务器的 HTML 内容会在任何 <script> 标签加载之前逐步显示出来。
🌐 Streaming does not need to wait for React itself to load in the browser, or for your app to become interactive. The HTML content from the server will get progressively revealed before any of the <script> tags load.
指定进入 shell 的内容
🌐 Specifying what goes into the shell
你的应用中任何 <Suspense> 边界之外的部分称为外壳:
🌐 The part of your app outside of any <Suspense> boundaries is called the shell:
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<BigSpinner />}>
<Sidebar>
<Friends />
<Photos />
</Sidebar>
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</Suspense>
</ProfileLayout>
);
}它决定了用户可能看到的最早加载状态:
🌐 It determines the earliest loading state that the user may see:
<ProfileLayout>
<ProfileCover />
<BigSpinner />
</ProfileLayout>如果你将整个应用封装在根部的 <Suspense> 边界中,shell 将只包含那个加载指示器。然而,这并不是一个愉快的用户体验,因为在屏幕上看到一个大加载指示器会让人感觉比多等一会儿看到真实布局更慢、更烦人。这就是为什么通常你会希望放置 <Suspense> 边界,使得 shell 感觉最小但完整——就像整个页面布局的骨架。
🌐 If you wrap the whole app into a <Suspense> boundary at the root, the shell will only contain that spinner. However, that’s not a pleasant user experience because seeing a big spinner on the screen can feel slower and more annoying than waiting a bit more and seeing the real layout. This is why usually you’ll want to place the <Suspense> boundaries so that the shell feels minimal but complete—like a skeleton of the entire page layout.
对 renderToReadableStream 的异步调用将在整个 shell 渲染完成后解析为 stream。通常,你会通过创建并返回带有该 stream 的响应来开始流式传输:
🌐 The async call to renderToReadableStream will resolve to a stream as soon as the entire shell has been rendered. Usually, you’ll start streaming then by creating and returning a response with that stream:
async function handler(request) {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js']
});
return new Response(stream, {
headers: { 'content-type': 'text/html' },
});
}到 stream 返回时,嵌套 <Suspense> 边界中的组件可能仍在加载数据。
🌐 By the time the stream is returned, components in nested <Suspense> boundaries might still be loading data.
服务器上的日志记录崩溃
🌐 Logging crashes on the server
默认情况下,服务器上的所有错误都会记录到控制台。你可以覆盖此行为来记录崩溃报告:
🌐 By default, all errors on the server are logged to console. You can override this behavior to log crash reports:
async function handler(request) {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js'],
onError(error) {
console.error(error);
logServerCrashReport(error);
}
});
return new Response(stream, {
headers: { 'content-type': 'text/html' },
});
}如果你提供自定义的 onError 实现,也别忘了像上面一样将错误记录到控制台。
🌐 If you provide a custom onError implementation, don’t forget to also log errors to the console like above.
从 shell 中的错误中恢复
🌐 Recovering from errors inside the shell
在这个例子中,壳包含 ProfileLayout、ProfileCover 和 PostsGlimmer:
🌐 In this example, the shell contains ProfileLayout, ProfileCover, and PostsGlimmer:
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}如果在渲染这些组件时发生错误,React 将没有任何有意义的 HTML 可以发送给客户端。将你的 renderToReadableStream 调用封装在一个 try...catch 中,以便在最后手段时发送不依赖服务器渲染的备用 HTML:
🌐 If an error occurs while rendering those components, React won’t have any meaningful HTML to send to the client. Wrap your renderToReadableStream call in a try...catch to send a fallback HTML that doesn’t rely on server rendering as the last resort:
async function handler(request) {
try {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js'],
onError(error) {
console.error(error);
logServerCrashReport(error);
}
});
return new Response(stream, {
headers: { 'content-type': 'text/html' },
});
} catch (error) {
return new Response('<h1>Something went wrong</h1>', {
status: 500,
headers: { 'content-type': 'text/html' },
});
}
}如果在生成 shell 时发生错误,onError 和你的 catch 块都会触发。使用 onError 进行错误报告,并使用 catch 块发送回退的 HTML 文档。你的回退 HTML 不必是错误页面。相反,你可以包含一个仅在客户端渲染你的应用的备用 shell。
🌐 If there is an error while generating the shell, both onError and your catch block will fire. Use onError for error reporting and use the catch block to send the fallback HTML document. Your fallback HTML does not have to be an error page. Instead, you may include an alternative shell that renders your app on the client only.
从 shell 外部的错误中恢复
🌐 Recovering from errors outside the shell
在这个示例中,<Posts /> 组件被封装在 <Suspense> 中,因此它不是 shell 的一部分:
🌐 In this example, the <Posts /> component is wrapped in <Suspense> so it is not a part of the shell:
function ProfilePage() {
return (
<ProfileLayout>
<ProfileCover />
<Suspense fallback={<PostsGlimmer />}>
<Posts />
</Suspense>
</ProfileLayout>
);
}如果 Posts 组件或其内部的某个地方发生错误,React 将尝试从中恢复:
🌐 If an error happens in the Posts component or somewhere inside it, React will try to recover from it:
- 它将把最近的
<Suspense>边界 (PostsGlimmer) 的加载回退发出到 HTML 中。 - 它将“不再尝试”在服务器上渲染
Posts内容。 - 当 JavaScript 代码在客户端加载时,React 将在客户端重试渲染
Posts。
如果在客户端重试渲染 Posts 也 失败,React 将在客户端抛出错误。与渲染期间抛出的所有错误一样,最近的父级错误边界 决定如何向用户展示错误。实际上,这意味着用户将看到加载指示器,直到确认该错误无法恢复为止。
🌐 If retrying rendering Posts on the client also fails, React will throw the error on the client. As with all the errors thrown during rendering, the closest parent error boundary determines how to present the error to the user. In practice, this means that the user will see a loading indicator until it is certain that the error is not recoverable.
如果在客户端重试渲染 Posts 成功,来自服务器的加载备用内容将被客户端渲染输出替换。用户不会知道服务器出现了错误。然而,服务器 onError 回调和客户端 onRecoverableError 回调将会触发,以便你可以收到错误通知。
🌐 If retrying rendering Posts on the client succeeds, the loading fallback from the server will be replaced with the client rendering output. The user will not know that there was a server error. However, the server onError callback and the client onRecoverableError callbacks will fire so that you can get notified about the error.
设置状态码
🌐 Setting the status code
流式传输引入了一种权衡。你希望尽早开始流式传输页面,以便用户可以更快看到内容。然而,一旦开始流式传输,就无法再设置响应状态码。
🌐 Streaming introduces a tradeoff. You want to start streaming the page as early as possible so that the user can see the content sooner. However, once you start streaming, you can no longer set the response status code.
通过将你的应用划分为外壳(尤其是 <Suspense> 边界之上)和其余内容,你已经解决了这个问题的一部分。如果外壳出现错误,你的 catch 块将运行,这允许你设置错误状态码。否则,你就知道应用可能在客户端恢复,因此你可以发送 “OK”。
🌐 By dividing your app into the shell (above all <Suspense> boundaries) and the rest of the content, you’ve already solved a part of this problem. If the shell errors, your catch block will run which lets you set the error status code. Otherwise, you know that the app may recover on the client, so you can send “OK”.
async function handler(request) {
try {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js'],
onError(error) {
console.error(error);
logServerCrashReport(error);
}
});
return new Response(stream, {
status: 200,
headers: { 'content-type': 'text/html' },
});
} catch (error) {
return new Response('<h1>Something went wrong</h1>', {
status: 500,
headers: { 'content-type': 'text/html' },
});
}
}如果一个组件位于外壳之外(即在 <Suspense> 边界内)抛出错误,React 不会停止渲染。这意味着 onError 回调会被触发,但你的代码将继续运行而不会进入 catch 块。这是因为 React 会尝试在客户端从该错误中恢复,如上所述。
🌐 If a component outside the shell (i.e. inside a <Suspense> boundary) throws an error, React will not stop rendering. This means that the onError callback will fire, but your code will continue running without getting into the catch block. This is because React will try to recover from that error on the client, as described above.
但是,如果你愿意,你可以使用出现错误的事实来设置状态代码:
🌐 However, if you’d like, you can use the fact that something has errored to set the status code:
async function handler(request) {
try {
let didError = false;
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js'],
onError(error) {
didError = true;
console.error(error);
logServerCrashReport(error);
}
});
return new Response(stream, {
status: didError ? 500 : 200,
headers: { 'content-type': 'text/html' },
});
} catch (error) {
return new Response('<h1>Something went wrong</h1>', {
status: 500,
headers: { 'content-type': 'text/html' },
});
}
}这只会捕获在生成初始 shell 内容时发生的 shell 外部错误,因此它不是全面的。如果知道某些内容是否发生错误至关重要,你可以将其移入 shell 中。
🌐 This will only catch errors outside the shell that happened while generating the initial shell content, so it’s not exhaustive. If knowing whether an error occurred for some content is critical, you can move it up into the shell.
以不同的方式处理不同的错误
🌐 Handling different errors in different ways
你可以创建你自己的 Error 子类,并使用instanceof 操作符来检查抛出了哪种错误。例如,你可以定义一个自定义的 NotFoundError 并从你的组件中抛出它。然后你可以将错误保存在 onError 中,并根据错误类型在返回响应之前执行不同的操作:
🌐 You can create your own Error subclasses and use the instanceof operator to check which error is thrown. For example, you can define a custom NotFoundError and throw it from your component. Then you can save the error in onError and do something different before returning the response depending on the error type:
async function handler(request) {
let didError = false;
let caughtError = null;
function getStatusCode() {
if (didError) {
if (caughtError instanceof NotFoundError) {
return 404;
} else {
return 500;
}
} else {
return 200;
}
}
try {
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js'],
onError(error) {
didError = true;
caughtError = error;
console.error(error);
logServerCrashReport(error);
}
});
return new Response(stream, {
status: getStatusCode(),
headers: { 'content-type': 'text/html' },
});
} catch (error) {
return new Response('<h1>Something went wrong</h1>', {
status: getStatusCode(),
headers: { 'content-type': 'text/html' },
});
}
}请记住,一旦发出 shell 并开始流式传输,就无法更改状态代码。
🌐 Keep in mind that once you emit the shell and start streaming, you can’t change the status code.
等待为爬虫和静态生成加载所有内容
🌐 Waiting for all content to load for crawlers and static generation
流式提供了更好的用户体验,因为用户可以在内容可用时看到内容。
🌐 Streaming offers a better user experience because the user can see the content as it becomes available.
但是,当爬虫访问你的页面时,或者如果你在构建时生成页面,你可能希望首先加载所有内容,然后生成最终的 HTML 输出,而不是逐步显示它。
🌐 However, when a crawler visits your page, or if you’re generating the pages at the build time, you might want to let all of the content load first and then produce the final HTML output instead of revealing it progressively.
你可以通过等待 stream.allReady Promise 来等待所有内容加载完成:
🌐 You can wait for all the content to load by awaiting the stream.allReady Promise:
async function handler(request) {
try {
let didError = false;
const stream = await renderToReadableStream(<App />, {
bootstrapScripts: ['/main.js'],
onError(error) {
didError = true;
console.error(error);
logServerCrashReport(error);
}
});
let isCrawler = // ... depends on your bot detection strategy ...
if (isCrawler) {
await stream.allReady;
}
return new Response(stream, {
status: didError ? 500 : 200,
headers: { 'content-type': 'text/html' },
});
} catch (error) {
return new Response('<h1>Something went wrong</h1>', {
status: 500,
headers: { 'content-type': 'text/html' },
});
}
}常规访客会获得逐步加载的内容流。爬虫则会在所有数据加载完成后接收最终的 HTML 输出。然而,这也意味着爬虫必须等待所有数据,其中一些数据可能加载较慢或出错。根据你的应用,你也可以选择向爬虫发送壳体。
🌐 A regular visitor will get a stream of progressively loaded content. A crawler will receive the final HTML output after all the data loads. However, this also means that the crawler will have to wait for all data, some of which might be slow to load or error. Depending on your app, you could choose to send the shell to the crawlers too.
中止服务器渲染
🌐 Aborting server rendering
你可以在超时后强制服务器渲染“放弃”:
🌐 You can force the server rendering to “give up” after a timeout:
async function handler(request) {
try {
const controller = new AbortController();
setTimeout(() => {
controller.abort();
}, 10000);
const stream = await renderToReadableStream(<App />, {
signal: controller.signal,
bootstrapScripts: ['/main.js'],
onError(error) {
didError = true;
console.error(error);
logServerCrashReport(error);
}
});
// ...React 会将剩余的加载回退作为 HTML 刷新,并将尝试在客户端渲染其余部分。
🌐 React will flush the remaining loading fallbacks as HTML, and will attempt to render the rest on the client.