当前位置: 首页 > 工具软件 > react-desktop > 使用案例 >

react-react ssr

葛永丰
2023-12-01

react ssr的方法有很多,但在思路上基本都是可以互通的。本文提供一种 react ssr 的方法。

一、要达到什么功能?

  1. 既然是服务端渲染,那么本身就得是达到服务端渲染解决的一些固有问题,比如seo、直接输出已经被渲染成html文档片段
  2. 页面切换时,使用客户端渲染的路由;页面刷新时,使用服务端路由返回当前页面

二、分析功能

一、使用的技术栈

  • 开发依赖:webpack、webpack-cli等脚手架相关、babel、preset-react等编译相关、nodemon、npm-run-all等命令行工具相关、tsconfig
  • 公共部分:react相关、axios
  • 服务端部分:koa、koa-static、koa/router等服务端nodeJs框架
  • 客户端部分:公共部分包含大部分资源,没有必要引入特殊的前端库;但我把 store 部分归入客户端部分。

二、输出

触发输出的流程很简单:在nodeJs服务端接收页面的 url 请求后,分析路由,返回相对应路由的页面。

路由需要返回相对应页面,就需要在服务端解析对应的路由模块。react 提供了 StaticRouter 组件,在服务端解析路由组件。

返回的页面,希望能够直接创建出来对应的html文档片段,这正是服务端渲染的意义之一:比如我渲染一个列表页,纯前后端分离的做法是发请求,获得数据,然后渲染,能做的优化就是 loading + 占位骨架屏;而服务端渲染要直接把文档片段生成,大大减少页面抖动。此时要做的是:在服务端发送请求,在获得请求的结果之后,将数据通过 react 在服务端渲染。这是一个异步的过程,所以需要异步函数 async 等待结果。

正常情况下得到数据后,应该交由 react 渲染。但由于服务端不能生成 DOM ,所以使用 react 官方提供的 renderToString 方法,得到 string 类型的 html 文档片段,然后插入到 html 对应的 #app 中。

此处使用 axios 发送请求,发送请求的代码,完全可以是公共代码。

在 nodeJs 服务端发送一个 http 请求,这个请求也会被当作从当前页面发送的请求。但在 network 中信息展示不完整。

此时的 html 已经是要返回的结果了。

 三、客户端渲染

客户端渲染没有什么特别的地方,因为页面在被返回时已经是现成的页面了。

数据管理的部分也放在了客户端,因为大部分和操作数据有关的业务都在客户端;服务端只负责返回初始化数据。

四、数据

项目里肯定是有路由的,当切换路由时,一切变化发生在客户端,执行的是客户端的 js 代码,所以在客户端一定也会有发送请求的代码。

客户端肯定会通过某类数据管理中心去管理请求返回的数据的,比如 redux 、mobx 等,这就需要客户端的 store 和 服务端的 store 在输出时保持一致。

所以我们还需要在服务端返回数据时,将这个数据注入到返回的 html 中,然后客户端代码要去找到这个数据。

然后我在客户端的代码的数据初始值,也要直接取这个服务端输出的数据。

此时客户端 react 也会渲染一次,这其实就是正常的 react 初始化渲染过程,而且直接拿到服务端的数据,渲染的结果不会有任何出入,所以也不会有体验上的缺陷。

//    数据要转为字符串
const contextString = JSON.stringify(ssrData);
//    注入的数据
`<script>window.initialState=${contextString}</script>`

此时可以考虑一个需求:假设我返回了某个路由的页面,当我切换页面再回来时,是否需要发送请求获取数据(可能此时服务器数据变化了的情况)?还是依然使用页面初始的数据(不请求)?对于这两种问题应该如何处理。

其实对于这个问题,考虑的出发点不应该在数据上而是在路由上,如果我切换路由时把当前页面或者目标页面的数据清空了,那么就达到实时请求的需求;否则就可以拦截请求。


页面中的路由肯定会有很多,而服务端注入到 html 中的数据最好能保持统一,此时有2种做法:

  • 统一所有的注入数据,然后通过给注入数据区分命名空间,以控制路由和数据的对应关系。
  • 注入数据直接就按不同的命名注入,各个路由的 store 按自己的命名直接去匹配数据。

五、css

css 的话几乎完全可以按纯客户端流程去操作,当然,还有一些优化的余地。

服务端渲染图一个快,最快的那肯定是行内式,做 css in js ,但个人感觉这样做开发体验不好,维护成本高。

考虑一下纯客户端渲染,我们一般通过 mini-css-extract-plugin 包把 css 拉出去,给 class 加 hash等。它的渲染流程是,html 和 css 都匹配同一个 className,然后 js 加载完以后渲染 dom;而在 html 中创建一个 link 引入css。因此它分为 js 和 css 两布加载。

这里可以采用一部分创建内嵌式 css(做首屏优化),另一部分抽离 css 的方案。

方案:

  • 既然是一部分抽离,一部分内嵌,那么为了方便 loader 抽取,在开发上就会有一定的区分,我这里将内嵌的 css 文件名增加 .style 字段,比如 index.style.css
  • 在 webpack loader 中匹配 css 文件,对包含 .style 字段的文件用 style-loader 做内联样式,对其他 css 做 mini-css-extract-plugin 抽离。取不包含 .style 字段时,用 exclude 更快一点,至少配置更方便
  • 为了统一,我的服务端编译 webpack 代码和 客户端编译的 webpack 代码都是同一套配置,所以要注意区分 —— style-loader 的编译是要依赖 document 的。。。
  • post-css 、css-module、less 等不在考虑范畴,正常做就ok

对于第三点,事实上,一些服务端编译的过程是可以忽略的,因为服务端输出时是没有运行时的代码的 

还有一点要注意的是内敛 css 不能把所有的路由都打包,这个通过路由与组件的异步加载去解决,不要考虑在 css 上解决。

六、其他优化

区分环境:区分 dev 和 prod 环境

有此需求的地方包括:js、创建模板 html、cdn,其中 html 和 cdn 都是可以通过服务端控制的,而 js 需要通过 wepback mode:"production"配置。我通常不回去在 package.json 里写 env,而是直接区分 dev 命令 和 prod 命令,分别载入不同的 webpackjs,这样可以少在 webpack 中写太多区分环境的代码。

//    package.json
"build:dev:clent": "webpack --config build/client/webpack.client.dev.js",
"build:dev:server": "webpack --config build/server/webpack.server.dev.js",

cdn:配置 externals,然后用 cdn 方式引入 react 等,这属于 webpack 优化部分。

 类似资料: