目前我们前端使用两个Node服务 + 多个前端项目(react + webpack)。一个Node充当渲染服务;一个Node 对后端微服务做接口聚合充当node网关的作用;各业务块维护各自的前端项目。因前端项目跟node服务是分离的,所以需要一个适合自身架构的SSR方案。
=======================================================
前言
一直以来前端有几种场景是必须SSR(服务端渲染)来完成的,如SEO、分享页面到社交平台、弥补CSR(客户端渲染)首屏空白问题等。目前团队的前端项目由 node 来完成前端路由和静态资源的管理,偶尔有SSR的需要,是通过ejs把一些关键数据注入到HTML模板中来完成。因前后分离后前端项目已全面转至的 React,这样ejs就很难发挥作用,通常只用来注入一些关键的meta信息,无法胜任完整页面渲染的需求。现在流行的React SSR方案有Next.js、egg-react-ssr、umi等,这些方案都是node + react 全家桶式解决方案,支持约定式路由、开箱即用,但很难跟我们目前的架构相融合,那么就需要探索一个更适合我们现有架构的SSR方案。
(社交平台分享网页)
渲染流程概览
先来张图,这样就显得比较ok一些
![b4af3511ff36a81d96b99209d79ee2eb.png](https://i-blog.csdnimg.cn/blog_migrate/bb3540b059c49f97fa430bda5309b40e.jpeg)
- 浏览器访问url,node-web路由匹配到对应ssr controller
- 读取front-web上 serverFun并调用,返回HTML文档。
- 调用getComponent根据路由匹配出对应的页面组件PageComponent
- 调用PageComponent 的getInitialProps获取后端数据填充到store或props
- 提供一个serverFun暴露给node调用,serverFun内部通过renderToNodeStream完成页面渲染
- 浏览器端进行hydrate渲染进行事件绑定。
这样设计的目的是为了降低node-web与front-web耦合程度,突出node-web作为静态资源容器的特点。
具体实现
Node Web 端实现
![86e93020fb06ba89d3272d8b136d37dc.png](https://i-blog.csdnimg.cn/blog_migrate/6235b484bf50721bcf13fe10eec119b9.jpeg)
上面便是node-web中的核心代码:
- 读取 front-web/ssr/cfs-note-detail.js,调用ssr.script()把文件加入到File Watch,这样front-web更新时node-web就能够更新代码。
- 使用node vm虚拟机执行front-web 脚本,创建一个隔离的上下文ssrContext,可以mock 下window对象处理前端直接调用window对象引起的错误。
- 调front-web脚本暴露出来的getSSRStream方法得到HTML Stream 或 HTML String。
- 如果ssr执行失败使用ejs进行兜底渲染。
Front Web 端实现
webpack打包分为build:ssr和build:csr,使用webpack.DefinePlugin定义 __isBrowser__
用来区分。ssr入口文件SSR Entry
打包后的文件供node端调用,csr入口文件CSR Entry
在浏览器端执行用于ReactDOM.hydrate。
SSR Entry
![89ab8dfedf12e6cc4fbc909d0f11612b.png](https://i-blog.csdnimg.cn/blog_migrate/718f3cc0cd28c952977c9a1cf2363e76.jpeg)
上面便是front-web ssr脚本的核心代码:
- serverRender 方法:
- 根据routes 和 ctx.path 获取当前的页面组件ActiveComponent
- 如果ActiveComponent 存在getInitialProps方法则调用后,传入到store或props
- Layout组件用于更改meta、注入hydrate需要的css和js资源
2. ssrConfig 配置:
-
- type: 指定渲染方式'ssr'和'csr', 当指定csr时serverRender不会被调用,只将css/js注入到Layout,然后再浏览器端完成页面渲染
- injectCss、injectScript: SSR完成后在浏览器进行hydrate渲染
- serverJs、layoutJs: 调用renderToStream方法时,内部调用serverJs(ssr)或 layoutJs(csr)
3. getSSRStream: 暴露给node调用,返回HTML Stream 或 HTML String
Layout Component
![65c9ac5cc536a1376734767c976123f0.png](https://i-blog.csdnimg.cn/blog_migrate/77c6871bfc51999134f27f37c375b2ab.jpeg)
Layout 组件的作用是服务端调用时返回 HTML template
SSR模式下Layout主要做了四件事:
- 注入hyrate所需的css/js
- 把页面组件getInitailProps()的返回值和store[mobx、redux等]组合成serverData放到全局变量
window.__INITIAL_DATA__
- 设置全局变量
window.__USE_SSR__ = ture
- 更改meta信息
CSR 模式下Layout只是把css/js注入到模板中
CSR Entry
![a8792113e56cc21443edf430fb856416.png](https://i-blog.csdnimg.cn/blog_migrate/eb0304ddeb3d09c1f94fd1f9ae1d8df5.jpeg)
CSR Entry 打包的结果在浏览器端执行,csr模式下执行ReactDOM.render,ssr模式下执行ReactDOM.hydrate。 同时调用了getWrappedComponent高阶组件,它的作用是:ssr模式下把window.__INITIAL_DATA__
挂载到props,用于保证ReactDOM.hydrate的有效性;csr模式下调用页面组件的getInitialProps。
PageComponent
PageComponent中添加静态方法getInitialProps:
![4e9871d9693d97eab6070123c36db15b.png](https://i-blog.csdnimg.cn/blog_migrate/35fbb17f9ece7f7aebcc0566193d42fa.jpeg)
总结
方案优点:
- node-web 与 front-web解耦增加了node-web的扩展性,例如 node-web + front-web-2、node-web + front-web-3等可以使用相同方式接入,而不是每个站点各启一个node服务。
- 服务端和客户端共用代码,ssr与csr通过config进行随时切换
缺点:
- front-web需要把依赖全部打包到ssr/[page].js,无法externals node_modules、split chunks,打包文件较大
- node-web执行脚本过于黑盒,里面的逻辑略显复杂。
================2020.5.21 更新:服务端渲染失败兜底=====================
例如: /product/123445 重定向到 /product/123445?csr=1 进行客户端渲染
![091f2f2472b5cd2dcc7d02b1683cdfa6.png](https://i-blog.csdnimg.cn/blog_migrate/41695513a4d7246ad2456bb9e6d6b008.jpeg)