概览
允许你将组件渲染成静态HTML标签用于服务端进行渲染
不支持流的环境
-
renderToString
const html = renderToString(reactNode)- 将 React 元素渲染为 HTML
- 不支持流
- Suspense 有限支持,立即返回 fallback
- 可以在客户端使用不推荐
- 客户端使用 hydrate
- 如果在客户端转化组件为 HTML
import { createRoot } from 'react-dom/client'; import { flushSync } from 'react-dom'; const div = document.createElement('div'); const root = createRoot(div); flushSync(() => { root.render(<MyIcon />); }); console.log(div.innerHTML); // For example, "<svg>...</svg>"
-
renderToStaticMarkup
const html = renderToStaticMarkup(reactNode)-
渲染没有交互逻辑的组件为 HTML 字符串
import { renderToStaticMarkup } from 'react-dom/server'; // The route handler syntax depends on your backend framework app.use('/', (request, response) => { const html = renderToStaticMarkup(<Page />); response.send(html); }); -
React 作为静态页面生成工具或者渲染静态内容(Email)
-
客户端不能使用 hydrated
-
Node流环境
-
renderToPipeableStream
Node 环境使用,Demo或者其他运行时使用
renderToReadableStreamconst { pipe, abort } = renderToPipeableStream(reactNode, options?)渲染 react 组件为 HTML 转化为 Node 流
reactNode 节点 ,render 函数需要 return html 标签
-
options
bootstrapScriptContent、bootstrapModules、identifierPrefix、namespaceURI、nonce、onAllReady、onError、onShellError、progressiveChunkSize...
-
bootstrapScripts
注入
<script>标签数组 -
onShellReady
当 initial shell 渲染完后这个回调函数会执行,你能设置响应信息,开始使用流的相关功能。React 开始把其他内容变为流。script 标签替换 fallback
-
-
pipe HTML 转化为 Writable Node.js Stream
-
abort 停止服务端渲染然后渲染客户单剩下的内容
-
渲染react元素为原始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> ); } import { renderToPipeableStream } from 'react-dom/server'; // The route handler syntax depends on your backend framework app.use('/', (request, response) => { const { pipe } = renderToPipeableStream(<App />, { bootstrapScripts: ['/main.js'], onShellReady() { response.setHeader('content-type', 'text/html'); pipe(response); } }); }) // 客户端 import { hydrateRoot } from 'react-dom/client'; import App from './App.js'; hydrateRoot(document, <App />); -
加载时展示更多内容
使用 Suspense 延迟加载耗费时间的内容从而让其他内容显示
function ProfilePage() { return ( <ProfileLayout> <ProfileCover /> <Suspense fallback={<BigSpinner />}> <Sidebar> <Friends /> <Photos /> </Sidebar> <Suspense fallback={<PostsGlimmer />}> <Posts /> </Suspense> </Suspense> </ProfileLayout> ); } -
如何分割 shell
不被 Suspense 包裹的组件成为 shell
当 onShellReady 触发时,有可能 Suspense 还是加载状态
通过 Suspense 把应用分割为不同的 shell可以解决用户体验问题。
-
处理服务端报错信息
使用 onError 函数
const { pipe } = renderToPipeableStream(<App />, { bootstrapScripts: ['/main.js'], onShellReady() { response.setHeader('content-type', 'text/html'); pipe(response); }, onError(error) { console.error(error); logServerCrashReport(error); } }); -
shell 内部报错如何处理
使用 onShellReady 设置错误的响应信息
-
渲染组件报错,React 不会任何 HTML 到客户端
-
手动返回一些提示信息 onShellError
-
设置响应信息
-
-
shell 外部报错如何处理
- 触发 fallback 组件
- 服务端放弃渲染包裹组件
- 客户端重新渲染包裹组件
- 如果客户端报错就抛出,按照相关渲染逻辑进行。成功就会替换 fallback,触发服务端 onError 和客户端
onRecoverableError回调 - onShellReady 代替 onShellError 触发
-
可以重新定义状态码
let didError = false; const { pipe } = renderToPipeableStream(<App />, { bootstrapScripts: ['/main.js'], onShellReady() { response.statusCode = didError ? 500 : 200; response.setHeader('content-type', 'text/html'); pipe(response); }, onShellError(error) { response.statusCode = 500; response.setHeader('content-type', 'text/html'); response.send('<h1>Something went wrong</h1>'); }, onError(error) { didError = true; console.error(error); logServerCrashReport(error); } }); -
自定义处理错误逻辑
let didError = false; let caughtError = null; function getStatusCode() { if (didError) { if (caughtError instanceof NotFoundError) { return 404; } else { return 500; } } else { return 200; } } const { pipe } = renderToPipeableStream(<App />, { bootstrapScripts: ['/main.js'], onShellReady() { response.statusCode = getStatusCode(); response.setHeader('content-type', 'text/html'); pipe(response); }, onShellError(error) { response.statusCode = getStatusCode(); response.setHeader('content-type', 'text/html'); response.send('<h1>Something went wrong</h1>'); }, onError(error) { didError = true; caughtError = error; console.error(error); logServerCrashReport(error); } }); -
爬虫和静态网站生成需要等待所有内容加载完成
使用 onAllReady
let didError = false; let isCrawler = // ... depends on your bot detection strategy ... const { pipe } = renderToPipeableStream(<App />, { bootstrapScripts: ['/main.js'], onShellReady() { if (!isCrawler) { response.statusCode = didError ? 500 : 200; response.setHeader('content-type', 'text/html'); pipe(response); } }, onShellError(error) { response.statusCode = 500; response.setHeader('content-type', 'text/html'); response.send('<h1>Something went wrong</h1>'); }, onAllReady() { if (isCrawler) { response.statusCode = didError ? 500 : 200; response.setHeader('content-type', 'text/html'); pipe(response); } }, onError(error) { didError = true; console.error(error); logServerCrashReport(error); } }); -
停止服务端的渲染
const { pipe, abort } = renderToPipeableStream(<App />, { // ... }); setTimeout(() => { abort(); }, 10000);触发 fallback然后渲染客户端的其他内容
-
-
renderToNodeStream()(已废弃)
ReactDOMServer.renderToNodeStream(element)-
返回一个可输出 HTML 字符串utf-8 编码的可读流(iconv-lite)
-
在已有服务端渲染标记的节点上调用 ReactDOM.hydrate() 方法,React 将会保留该节点且只进行事件处理绑定
-
-
renderToStaticNodeStream
const stream = renderToStaticNodeStream(reactNode)-
渲染没有交互逻辑的组件为 Node.js Readable Stream
import { renderToStaticNodeStream } from 'react-dom/server'; // The route handler syntax depends on your backend framework app.use('/', (request, response) => { const stream = renderToStaticNodeStream(<Page />); stream.pipe(response); }); -
不能使用 hydrated
-
等待 Suspense 完成
-
这个方法缓存所有输出
-
返回内容是 utf-8 字节流
-
web 流(browsers, Deno, and some modern edge runtimes)
renderToReadableStream
const stream = await renderToReadableStream(reactNode, options?)
-
依赖 Web Streams,Node 环境使用 Readable Web Stream
-
渲染react元素为原始html
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' }, }); } } -
爬虫和静态网站生成需要等待所有内容加载完成
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' }, }); } } -
停止服务端的渲染
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); } }); // ...
获取 CSS 和 JS 文件路径
使用 bootstrapScripts,bootstrapScriptContent
// You'd need to get this JSON from your build tooling.
const assetMap = {
'styles.css': '/styles.123456.css',
'main.js': '/main.123456.js'
};
app.use('/', (request, response) => {
const { pipe } = renderToPipeableStream(<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']],
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
}
});
});
export default function App({ assetMap }) {
return (
<html>
<head>
...
<link rel="stylesheet" href={assetMap['styles.css']}></link>
...
</head>
...
</html>
);
}
import { hydrateRoot } from 'react-dom/client';
import App from './App.js';
hydrateRoot(document, <App assetMap={window.assetMap} />);
hydration 错误
- 生成的 HTML 有多余的空白字符
- 渲染逻辑使用 typeof window !== 'undefined'
- 渲染逻辑使用浏览器 API window.matchMedia
- 服务端和客户端渲染不同的数据