React SSR,如何使用脱水函数renderToPipeableStream()?
发布于 作者:苏南大叔 来源:程序如此灵动~接触到renderToString()
这个函数后,就可以理解到react
脱水的过程了。这里就引申到了一个用户体验的问题,用户浏览器总是期待服务器端有着最快的响应速度,除去硬件等因素外,react
官方推出了renderToPipeableStream()
这个函数,可以说是对renderToString()
的改进和优化。
苏南大叔的“程序如此灵动”,记录苏南大叔的编程心得体会。本文测试环境:nodejs@20.18.0,create-react-app@5.0.1,react-router-dom@6.27.0,react@18.3.1。本文主要讨论react
的脱水函数renderToPipeableStream()
的使用问题。
前文回顾
本文的代码改编自上一篇文章中的第三个有关express
的例子,参考:
renderToPipeableStream()
的使用基本原理就是:配合http
或者express
的pipe
使用,管道渐进式输出html
代码。
函数原型
下面的链接是react
的官方描述:
函数原型:
renderToPipeableStream(reactNode, options?)
这里的options
是用于配置流的对象的。下面的其属性表格,整理自react
官方文档。
可选参数 | 参数文字解释 |
---|---|
bootstrapScriptContent | 指定一个字符串,这个字符串将被放入<script> 标签中作为其内容。 |
bootstrapScripts | 一个 URL 字符串数组,它们将被转化为 <script> 标签嵌入页面。 |
bootstrapModules | 和 bootstrapScripts 相似,但是嵌入页面的是 <script type="module"> 。 |
identifierPrefix | 一个字符串前缀,用于由 useId 生成的 id 。在同一页面下的多人协作场景中会很有用。 |
namespaceURI | 一个字符串,指定与流相关联的 命名空间 URI 。默认是常规的 HTML 。 |
nonce | 一个字符串,能为脚本设置跨域限制,即 script-src 浏览器内容安全策略。 |
onShellReady | 一个回调函数,在 shell 初始化渲染后立即调用,渐进式流数据发送。 |
onAllReady | 一个回调函数,将会在所有渲染完成时触发,包括 shell 和所有额外的 content 。 |
onError | 一个回调函数,只要是出现了异常错误,无论这是 可恢复的 还是 不可恢复的,它都会触发。 |
onShellError | 一个回调函数,在初始化 shell 发生错误渲染时调用。 |
progressiveChunkSize | 一个块中的字节数。 |
苏南大叔看完官方说明后,就关注三个参数:
onShellReady
,每次渲染结束时机。【似乎非常切合这个函数的人社啊】onAllReady
,所有的渲染全部结束时机。【和这个函数的人设,是不是有冲突?】bootstrapScripts
,这个用于输出底部的那个特殊js
,可用也可以不用。
官方文档的内容很多,谈到了大量的特殊情况,很不符合本文的新手入门文档调性。所以,本文中,苏南大叔并不打算做更多深入描述。
发送的时机
react
脱水函数renderToPipeableStream()
,有两个回调函数时机(onShellReady
和onAllReady
)可以使用。经过代码实验,都可以用于发送(pipe(res)
)渲染结果。
官方说:pipe
将一段HTML
输出到Node.js
可写流中。如果你想启用流式传输,那么可以在onShellReady
中调用pipe
;如果要做爬虫和静态内容生成的话,那么可以在onAllReady
中调用它。
苏南大叔认为:大多数情况下,都应该在onShellReady()
中执行pipe(res)
。在onAllReady()
中执行pipe(res)
的话,就失去了这个函数的灵魂所在。所以,推荐的使用方式是:
const { pipe } = renderToPipeableStream(<App/>, {
onShellReady: () => {
pipe(res);
}
});
js文件的位置
由于官方提供了一个选项(bootstrapScripts
/bootstrapModules
),来输出一个js
文件地址。实际上就相当于react
项目尾部的那个js
文件的输出。
<script src="/ssr.js"></script>
实际使用的话,就会发现:
传统思路下,先输出html
的<div id='app'>
及之前,然后靠renderToPipeableStream()+bootstrapScripts
来输出渲染结果代码的话,这个js
的输出位置就会出现在<div id='app'>
的闭合代码之前,也就是说出现在div#app
内部,这显然是不合理的。
...
<div id='app>
react渲染结果
<script src="/ssr.js"></script>
</div>
...
期待值是:
...
<div id='app>
react渲染结果
</div>
<script src="/ssr.js"></script>
...
官方给出的思路是:
将模版代码,放在app
这个react
组件里面,然后整体作为<app/>
进行渲染,最后的js
代码就会出现在页面的</html>
标签之后。虽然说规避了上面所说的问题,但是,并不符合苏南大叔对js
加载位置的认知。【期待react
官方的后续改进】
<html>
...
<div id='app>
react渲染结果
</div>
...
</html>
<script src="/ssr.js"></script>
测试代码主体
测试代码的主体框架,是下面这样的。
import express from "express";
import React from "react";
import { Routes, Route, useParams } from "react-router-dom";
import { StaticRouter } from "react-router-dom/server";
import { renderToPipeableStream } from "react-dom/server";
function Post() {
var { id } = useParams();
return <div>No.{id} article</div>;
}
const app2 = express();
app2.use(express.static("dist"));
app2.get("/*", (req, res) => {
// 重点内容放这里
// app组件定义
// pipe渲染输出
});
app2.listen(3006);
执行方式是:
esno ssr.jsx
测试代码一【个人推荐】
res
先发送html
的开头部分代码,然后渲染react
的代码。然后在onShellReady()
里面管道输出渲染结果,最后发送html
的底部部分。
其实这里就主体部分是管道输出的。
const App = function () {
return (
<StaticRouter location={req.url}>
<Routes>
<Route path="/" element={<div>home</div>} />
<Route path="/post/:id" element={<Post />} />
</Routes>
</StaticRouter>
);
};
let data = {
www: "newsn.net",
author: "苏南大叔",
};
const htmlStart = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>React Server Side Rendering</title>
</head>
<body>
<div id="root">
`;
const htmlEnd = `
</div>
<script> window.__ROUTE_DATA__ = ${JSON.stringify(data)} </script>
<script src="/ssr.js"></script>
</body>
</html>
`;
res.write(htmlStart);
const { pipe } = renderToPipeableStream(<App />, {
onShellReady: () => {
pipe(res);
},
onAllReady: () => {
res.write(htmlEnd);
res.end();
},
});
测试代码二【官方推荐】
要把模版写成jsx
的形式,其实还是比较艰巨的。个人不是很喜欢,但是官方推荐。只能尽量规避哪些令人头晕的写法。参考文章:
const App = function () {
let data = {
www: "newsn.net",
author: "苏南大叔",
};
data = JSON.stringify(data);
return (
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>React Server Side Rendering</title>
</head>
<body>
<div id="root">
<StaticRouter location={req.url}>
<Routes>
<Route path="/" element={<div>home</div>} />
<Route path="/post/:id" element={<Post />} />
</Routes>
</StaticRouter>
</div>
<script
dangerouslySetInnerHTML={{
__html: "window.__ROUTE_DATA__ = " + data,
}}
></script>
<script src="/ssr.js"></script>
</body>
</html>
);
};
const { pipe } = renderToPipeableStream(<App />, {
onShellReady: () => {
res.setHeader("content-type", "text/html");
pipe(res);
},
});
测试代码三【也许不错】
意料外的是:这个renderToPipeableStream()
的渲染输出,会自带tidy
效果。
const App = function () {
return (
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>React Server Side Rendering</title>
</head>
<body>
<div id="root">
<StaticRouter location={req.url}>
<Routes>
<Route path="/" element={<div>home</div>} />
<Route path="/post/:id" element={<Post />} />
</Routes>
</StaticRouter>
</div>
</body>
</html>
);
};
let data = {
www: "newsn.net",
author: "苏南大叔",
};
data = JSON.stringify(data);
const { pipe } = renderToPipeableStream(<App />, {
bootstrapScriptContent: "window.__ROUTE_DATA__ = " + data,
bootstrapScripts: ["/ssr.js"],
onShellReady: () => {
res.setHeader("content-type", "text/html");
pipe(res);
},
});
这个方案里面,就是投个机取个巧,治标不治本。不过,对于很框架型的react
代码项目来说,可能是个不错的解决方案。
结束语
苏南大叔其实挺想不写这个结束语部分的,但是偶尔发现有个新的站点,又又又把我的全部内容给巴拉走了。不过,保留了基本人设,和文章里面的链接。所以,还是加个链接吧。万一被留下来了呢?
欢迎查看苏南大叔的react
相关经验文章:
本博客不欢迎:各种镜像采集行为。请尊重原创文章内容,转载请保留作者链接。