我们相信:世界是美好的,你是我也是。平行空间的世界里面,不同版本的生活也在继续...

接触到renderToString()这个函数后,就可以理解到react脱水的过程了。这里就引申到了一个用户体验的问题,用户浏览器总是期待服务器端有着最快的响应速度,除去硬件等因素外,react官方推出了renderToPipeableStream()这个函数,可以说是对renderToString()的改进和优化。

苏南大叔:React SSR,如何使用脱水函数renderToPipeableStream()? - renderToPipeableStream
React SSR,如何使用脱水函数renderToPipeableStream()?(图4-1)

苏南大叔的“程序如此灵动”,记录苏南大叔的编程心得体会。本文测试环境: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或者expresspipe使用,管道渐进式输出html代码。

函数原型

下面的链接是react的官方描述:

函数原型:

renderToPipeableStream(reactNode, options?)

这里的options是用于配置流的对象的。下面的其属性表格,整理自react官方文档。

可选参数参数文字解释
bootstrapScriptContent指定一个字符串,这个字符串将被放入<script>标签中作为其内容。
bootstrapScripts一个 URL 字符串数组,它们将被转化为 <script> 标签嵌入页面。
bootstrapModulesbootstrapScripts 相似,但是嵌入页面的是 <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(),有两个回调函数时机(onShellReadyonAllReady)可以使用。经过代码实验,都可以用于发送(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();
  },
});

苏南大叔:React SSR,如何使用脱水函数renderToPipeableStream()? - 分段输出
React SSR,如何使用脱水函数renderToPipeableStream()?(图4-2)

测试代码二【官方推荐】

要把模版写成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);
  },
});

苏南大叔:React SSR,如何使用脱水函数renderToPipeableStream()? - 使用方式二
React SSR,如何使用脱水函数renderToPipeableStream()?(图4-3)

测试代码三【也许不错】

意料外的是:这个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 SSR,如何使用脱水函数renderToPipeableStream()? - 自带tidy效果
React SSR,如何使用脱水函数renderToPipeableStream()?(图4-4)

这个方案里面,就是投个机取个巧,治标不治本。不过,对于很框架型的react代码项目来说,可能是个不错的解决方案。

结束语

苏南大叔其实挺想不写这个结束语部分的,但是偶尔发现有个新的站点,又又又把我的全部内容给巴拉走了。不过,保留了基本人设,和文章里面的链接。所以,还是加个链接吧。万一被留下来了呢?

欢迎查看苏南大叔的react相关经验文章:

如果本文对您有帮助,或者节约了您的时间,欢迎打赏瓶饮料,建立下友谊关系。
本博客不欢迎:各种镜像采集行为。请尊重原创文章内容,转载请保留作者链接。

 【福利】 腾讯云最新爆款活动!1核2G云服务器首年50元!

 【源码】本文代码片段及相关软件,请点此获取更多信息

 【绝密】秘籍文章入口,仅传授于有缘之人   react