renderToReadableStream
将 React 树渲染到 可读网络流。
¥renderToReadableStream
renders a React tree to a Readable Web Stream.
const stream = await renderToReadableStream(reactNode, options?)
参考
¥Reference
renderToReadableStream(reactNode, options?)
调用 renderToReadableStream
将你的 React 树作为 HTML 渲染到 可读网络流。
¥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>
标签。¥
reactNode
: A React node you want to render to HTML. For example, a JSX element like<App />
. It is expected to represent the entire document, so theApp
component should render the<html>
tag. -
可选
options
:具有流选项的对象。¥optional
options
: An object with streaming options.-
可选
bootstrapScriptContent
:如果指定,此字符串将放置在内联<script>
标记中。¥optional
bootstrapScriptContent
: If specified, this string will be placed in an inline<script>
tag. -
可选
bootstrapScripts
:要在页面上发出的<script>
标记的字符串 URL 数组。使用它来包含调用hydrateRoot
。 的<script>
如果你根本不想在客户端上运行 React,请忽略它。¥optional
bootstrapScripts
: An array of string URLs for the<script>
tags to emit on the page. Use this to include the<script>
that callshydrateRoot
. Omit it if you don’t want to run React on the client at all. -
可选
bootstrapModules
:像bootstrapScripts
,但发出<script type="module">
。¥optional
bootstrapModules
: LikebootstrapScripts
, but emits<script type="module">
instead. -
可选
identifierPrefix
:React 用于useId
。 生成的 ID 的字符串前缀 有助于避免在同一页面上使用多个根时发生冲突。必须与传递给hydrateRoot
。 的前缀相同¥optional
identifierPrefix
: A string prefix React uses for IDs generated byuseId
. Useful to avoid conflicts when using multiple roots on the same page. Must be the same prefix as passed tohydrateRoot
. -
可选
namespaceURI
:流的根为 名称空间 URI 的字符串。默认为常规 HTML。为 SVG 传递'http://www.w3.org/2000/svg'
或为 MathML 传递'http://www.w3.org/1998/Math/MathML'
。¥optional
namespaceURI
: A string with the root namespace URI for the stream. Defaults to regular HTML. Pass'http://www.w3.org/2000/svg'
for SVG or'http://www.w3.org/1998/Math/MathML'
for MathML. -
可选
nonce
:允许script-src
Content-Security-Policy 脚本的nonce
字符串。¥optional
nonce
: Anonce
string to allow scripts forscript-src
Content-Security-Policy. -
可选
onError
:每当出现服务器错误时都会触发的回调,无论是 recoverable 还是 不。 默认情况下,这只会调用console.error
。如果你将其改写为 记录崩溃报告,,请确保你仍然调用console.error
。你也可以在触发 shell 之前将它用于 调整状态码。¥optional
onError
: A callback that fires whenever there is a server error, whether recoverable or not. By default, this only callsconsole.error
. If you override it to log crash reports, make sure that you still callconsole.error
. You can also use it to adjust the status code before the shell is emitted. -
可选
progressiveChunkSize
:块中的字节数。阅读有关默认启发式的更多信息。¥optional
progressiveChunkSize
: The number of bytes in a chunk. Read more about the default heuristic. -
可选
signal
:一个 中止信号,它允许你 中止服务器渲染 并在客户端上渲染其余部分。¥optional
signal
: An abort signal that lets you abort server rendering and render the rest on the client.
-
返回
¥Returns
renderToReadableStream
返回一个 Promise:
¥renderToReadableStream
returns a Promise:
-
如果渲染 shell 成功,则 Promise 将解析为 可读网络流。
¥If rendering the shell is successful, that Promise will resolve to a Readable Web Stream.
-
如果渲染 shell 失败,Promise 将被拒绝。使用它来输出回退 shell。
¥If rendering the shell fails, the Promise will be rejected. Use this to output a fallback shell.
返回的流有一个额外的属性:
¥The returned stream has an additional property:
-
allReady
:当所有渲染完成时解析的 Promise,包括 shell 和所有额外的 内容。 你可以在返回响应之前await stream.allReady
用于爬虫和静态生成。 如果你这样做,你将不会获得任何渐进式加载。该流将包含最终的 HTML。¥
allReady
: A Promise that resolves when all rendering is complete, including both the shell and all additional content. You canawait stream.allReady
before returning a response for crawlers and static generation. If you do that, you won’t get any progressive loading. The stream will contain the final HTML.
用法
¥Usage
将 React 树作为 HTML 渲染到可读的 Web 流
¥Rendering a React tree as HTML to a Readable Web Stream
调用 renderToReadableStream
将你的 React 树作为 HTML 渲染到 可读网络流:
¥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' },
});
}
除了 root component,你还需要提供 bootstrap <script>
paths 的列表。你的根组件应返回整个文档,包括根 <html>
标记。
¥Along with the root component, you need to provide a list of bootstrap <script>
paths. Your root component should return the entire document including the root <html>
tag.
例如,它可能看起来像这样:
¥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 流中:
¥React will inject the doctype and your bootstrap <script>
tags into the resulting HTML stream:
<!DOCTYPE html>
<html>
<!-- ... HTML from your components ... -->
</html>
<script src="/main.js" async=""></script>
在客户端,你的引导脚本应该是 通过调用 hydrateRoot
来补充整个 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 中是行不通的。为了让它们远离你的源代码,你的根组件可以从作为属性传递的映射中读取真实文件名:
¥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} />
并通过资源 URL 传递你的 assetMap
:
¥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
,因此没有水合作用错误。
¥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 以及用该 HTML 替换加载回退的内联 <script>
标记。从用户的角度来看,该页面将首先出现 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.
Streaming 不需要等待 React 本身在浏览器中加载,也不需要等待你的应用变为交互式。在加载任何 <script>
标记之前,来自服务器的 HTML 内容将逐渐显示。
¥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.
¥Read more about how streaming HTML works.
指定进入 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>
边界中,外壳将仅包含该加载控件。然而,这并不是一种令人愉快的用户体验,因为在屏幕上看到一个大的旋转器比多等一会儿看到真正的布局感觉更慢、更烦人。这就是为什么通常你会希望放置 <Suspense>
边界,使外壳感觉最小但完整 - 就像整个页面布局的骨架。
¥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.
整个 shell 渲染完成后,对 renderToReadableStream
的异步调用将解析为 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
在此示例中,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 中。¥It will emit the loading fallback for the closest
<Suspense>
boundary (PostsGlimmer
) into the HTML. -
它将 “放弃” 尝试在服务器上渲染
Posts
内容。¥It will “give up” on trying to render the
Posts
content on the server anymore. -
当 JavaScript 代码加载到客户端时,React 将重试在客户端上渲染
Posts
。¥When the JavaScript code loads on the client, React will retry rendering
Posts
on the client.
如果在客户端重试渲染 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.
通过 划分你的应用 进入 shell(在所有 <Suspense>
边界之上)和其余内容,你已经解决了这个问题的一部分。如果 shell 出错,你的 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 输出。然而,这也意味着爬虫将不得不等待所有数据,其中一些可能加载缓慢或出错。根据你的应用,你也可以选择将 shell 发送给爬虫。
¥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.