前段韶光一贯在研究 react ssr 技能,然后写了一个完全的 ssr 开拓骨架。本日写文,紧张是把我的研究成果的精华内容整理落地,其余通过再次梳理希望创造更多优化的地方,也希望可以让更多的人少踩一些坑,让更多的人理解和节制这个技能。
相信看过本文(条件是能对你的胃口,也能较好的消化接管)你一定会对 react ssr 做事端渲染技能有一个深入的理解,可以打造自己的脚手架,更可以用来改造自己的实际项目,当然这不仅限于 react ,其他框架都一样,毕竟事理都是相似的。
为什么要做事端渲染(ssr)

至于为什么要做事端渲染,我相信大家都有所闻,而且每个人都能说出几点来。
首屏等待
在 SPA 模式下,所有的数据要乞降 Dom 渲染都在浏览器端完成,以是当我们第一次访问页面的时候很可能会存在“白屏”等待,而做事端渲染所有数据要乞降 html内容已在做事端处理完成,浏览器收到的是完全的 html 内容,可以更快的看到渲染内容,在做事端完成数据要求肯定是要比在浏览器端效率要高的多。
没考虑SEO的感想熏染
有些网站的流量来源紧张还是靠搜索引擎,以是网站的 SEO 还是很主要的,而 SPA 模式对搜索引擎不足友好,要想彻底办理这个问题只能采取做事端直出。改变不了别人(搜索yinqing),只能改变自己。
SSR + SPA 体验升级
只实现 SSR 实在没啥意义,技能上没有任何发展和进步,否则 SPA 技能就不会涌现。
但是纯挚的 SPA 又不足完美,以是最好的方案便是这两种体验和技能的结合,第一次访问页面是做事端渲染,基于第一次访问后续的交互便是 SPA 的效果和体验,还不影响 SEO 效果,这就有点完美了。
纯挚实现 ssr 很大略,毕竟这是传统技能,也不分措辞,随便用 php 、jsp、asp、node 等都可以实现。
但是要实现两种技能的结合,同时可以最大限度的重用代码(同构),减少开拓掩护本钱,那就须要采取 react 或者 vue 等前端框架相结合 node(ssr) 来实现。
本文紧张说 ReactSSR技能 ,当然 vue 也一样,只是技能栈不同而已。
核心事理
整体来说 react 做事端渲染事理不繁芜,个中最核心的内容便是同构。
node server 吸收客户端要求,得到当前的 req url path ,然后在已有的路由表内查找到对应的组件,拿到须要要求的数据,将数据作为 props 、 context 或者 store 形式传入组件,然后基于 react 内置的做事端渲染api renderToString()orrenderToNodeStream() 把组件渲染为 html字符串 或者 stream流 , 在把终极的 html 进行输出前须要将数据注入到浏览器端(注水),server 输出(response)后浏览器端可以得到数据(脱水),浏览器开始进行渲染和节点比拟,然后实行组件的 componentDidMount 完成组件内事宜绑定和一些交互,浏览看重用了做事端输出的 html节点 ,全体流程结束。
技能点确实不少,但更多的是架构和工程层面的,须要把各个知识点进行链接和整合。
这里放一个架构图
react ssr
从 ejs 开始
实现 ssr 很大略,先看一个 node ejs 的栗子。
jsx 到字符串
上面我们结合 ejs模板引擎 ,实现了一个做事端渲染的输出,html 和 数据直接输出到客户端。
参考以上,我们结合 react组件 来实现做事端渲染直出,利用 jsx 来代替 ejs ,之前是在 html 里利用 ejs 来绑天命据,现在改写成利用 jsx 来绑天命据,利用 react 内置 api 来把组件渲染为 html 字符串,其他没有差别。
为什么react 组件可以被转换为 html字符串呢?
大略的说我们写的 jsx 看上去就像在写 html(实在写的是工具) 标签,实在经由编译后都会转换成 React.createElement 方法,终极会被转换成一个工具(虚拟DOM),而且和平台无关,有了这个工具,想转换成什么那就看心情了。
ps:以上代码不能直接运行,须要结合babel 利用 @babel/preset-react 进行转换
引出问题
在上面非常大略的便是实现了 react ssr ,把 jsx 作为模板引擎,不要鄙视上面的一小段代码,他可以帮我们引出一系列的问题,这也是完全实现 react ssr 的基石。
双端路由如何掩护?首先我们会创造我在 server 端定义了路由 '/',但是在 react SPA 模式下我们须要利用 react-router 来定义路由。那是不是就须要掩护两套路由呢?
获取数据的方法和逻辑写在哪里?创造数据获取的 fetch 写的独立的方法,和组件没有任何关联,我们更希望的是每个路由都有自己的 fetch 方法。
做事端 html 节点无法重用虽然组件在做事端得到了数据,也能渲染到浏览器内,但是当浏览器端进行组件渲染的时候直出的内容会一闪而过消逝。
好了,问题有了,接下来我们就一步一步的来办理这些问题。
同构才是核心
react ssr 的核心便是同构,没有同构的 ssr 是没故意义的。
所谓同构便是采取一套代码,构建双端(server 和 client)逻辑,最大限度的重用代码,不用掩护两套代码。而传统的做事端渲染是无法做到的,react 的涌现冲破了这个瓶颈,并且现在已经得到了比较广泛的运用。
路由同构
双端利用同一套路由规则, node server 通过 req url path 进行组件的查找,得到须要渲染的组件。
//组件和路由配置 ,供双端利用 routes-config.js
//客户端 路由组件
node server 进行组件查找
路由匹配实在便是对 组件 path 规则的匹配,如果规则不繁芜可以自己写,如果情形很多种还是利用官方供应的库来完成。
matchRoutes(routes,pathname)
可以看下 matchRoutes方法 的返回值,个中 route.component 便是 要渲染的组件
react-router-config 这个库由react 官方掩护,功能是实现嵌套路由的查找,代码没有多少,有兴趣可以看看。
文章走到这里,相信你已经知道了路由同构,以是上面的第一个问题 :【双端路由如何掩护?】 办理了。
数据同构(预取同构)
这里开始办理我们最开始创造的第二个问题 - 【获取数据的方法和逻辑写在哪里?】
数据预取同构,办理双端如何利用同一套数据要求方法来进行数据要求。
先说下流程,在查找到要渲染的组件后,须要预先得到此组件所须要的数据,然后将数据通报给组件后,再进行组件的渲染。
我们可以通过给组件定义静态方法来处理,组件内定义异步数据要求的方法也通情达理,同时声明为静态(static),在 server 端和组件内都也可以直接通过组件(function) 来进行访问。
比如 Index.getInitialProps
其余还有在声明路由的时候把数据要求方法关联到路由中,比如定一个 loadData 方法,然后在查找到路由后就可以判断是否存在 loadData 这个方法。
看下参考代码
上面这种办法实现上没什么问题,但从职责划分的角度来说有些不足清晰,我还是比较喜好直接通过组件来得到异步方法。
好了,到这里我们的第二个问题 - 【获取数据的方法和逻辑写在哪里?】 办理了。
渲染同构
假设我们现在基于上面已经实现的代码,同时我们也利用 webpack 进行了配置,对代码进行了转换和打包,全体做事可以跑起来。
路由能够精确匹配,数据预取正常,做事端可以直出组件的 html ,浏览器加载 js 代码正常,查看网页源代码能看到 html 内容,彷佛我们的全体流程已经走完。
但是当浏览器真个 js 实行完成后,创造数据重新要求了,组件的重新渲染导致页面看上去有些闪烁。
这是由于在浏览器端,双端节点比拟失落败,导致组件重新渲染,也便是只有当做事端和浏览器端渲染的组件具有相同的 props 和 DOM 构造的时候,组件才能只渲染一次。
刚刚我们实现了双真个数据预取同构,但是数据也仅仅是做事端有,浏览器端是没有这个数据,当客户端进行首次组件渲染的时候没有初始化的数据,渲染出的节点肯定和做事端直出的节点不同,导致组件重新渲染。
数据注水
在做事端将预取的数据注入到浏览器,使浏览器端可以访问到,客户端进行渲染前将数据传入对应的组件即可,这样就担保了 props 的同等。
须要借助 ejs 模板,将数据绑定到页面上,为了防止 XSS 攻击,这里我把数据写到了 textarea 标签里。
下图中,我看着明文数据难熬痛苦,对数据做了base64编码 ,用之前须要转码,看个人须要。
数据脱水
上一步数据已经注入到了浏览器端,这一步要在客户端组件渲染前先拿到数据,并且传入组件就可以了。
客户端可以直策应用 id=krs-server-render-data-BOX 进行数据获取。
第一个方法大略粗暴,可直接在组件内的 constructor布局函数 内进行获取,如果怕代码重复,可以写一个高阶组件。
第二个方法可以通过 context 通报,只须要在入口处传入,在组件中声明 staticcontextType 即可。
我是采取context 通报,为了后面方便集成 redux 状态管理 。
行文至此,核心的内容已经基本说完,剩下的便是组件内如何利用脱水的数据。
下面通过 context 拿到数据 , 代码仅供参考,可根据自己的需求来进行封装和调度。
到此我们的第三个问题:【做事端 html 节点无法重用 】已经办理,但人不足完美,请连续看。
css 过滤
我们在写组件的时候大部分都会导入干系的 css 文件。
但是这个 css 文件在做事端无法实行,实在想想在做事端本来就不须要渲染 css 。为什么不直接干掉?所以为了方便,我这里写了一个 babel 插件,在编译的时候干掉 css 的导入代码。
动态路由的 SSR
现在要说一个更加核心的内容,也是本文的一个压轴亮点,可以说是 全网唯一 ,我之前也看过很多文章和资料都没有细说这一块儿的实现。
不知道你有没有创造,上面我们已经一步一步的实现了 ReactSSR同构 的完全流程,但是总觉得少点什么东西。
SPA 模式下大部分都会实现组件分包和按需加载,防止所有代码打包在一个文件过大影响页面的加载和渲染,影响用户体验。
那么基于 SSR 的组件按需加载如何实现呢?
当然我们所限定按需的粒度是路由级别的,要求不同的路由动态加载对应的组件。
如何实现组件的按需加载?
在 webpack2 期间紧张利用 require.ensure 方法来实现按需加载,他会单独打包指定的文件,在当下 webpack4 ,有了更加规范的的办法实现按需加载,那便是动态导入 import('./xx.js') ,当然实现的效果和 require.ensure 是相同的。
咱们这里只说如何借助这个规范实现按需加载的路由,关于动态导入的实现事理先按下不表。
我们都知道 import 方法传入一个js文件地址,返回值是一个 promise 工具,然后在 then 方法内回调得到按需的组件。他的事理实在便是通过 jsonp 的办法,动态要求脚本,然后在回调内得到组件。
那现在我们已经得到了几个比较有用的信息。
如何加载脚本 - import结合webpack 自动完成脚本是否加载完成 - 通过在 then 方法回调进行处理获取异步按组件 - 通过在 then 方法回调内获取我们可以试着把上面的逻辑抽象成为一个组件,然后在路由配置的地方进行导入后,那么是不是就完成了组件的按需加载呢?
先看下按需加载组件, 目的是在 import 完成的时候得到按需的组件,然后变动容器组件的 state ,将这个 异步组件 进行渲染。
Async 容器组件吸收一个 props 传过来的 load 方法,返回值是 Promise 类型,用来动态导入组件。
在生命周期 UNSAFE_componentWillMount 得到按需的组件,并将组件存储到 state.COMPT 内,同时在 render 方法中止定这个状态的可用性,然后调用 this.props.children 方法进行渲染。
当然这只是个中一种方法,也有很多是通过 react-loadable库 来进行实现,但是实现思路基本相同,有兴趣的可以看下源码。
到这里我们已经实现了组件的按需加载,剩下便是配置到路由。
看下伪代码
结合路由的按需加载已经配置完成,先不管 server端 是否须要进行调度,此时的代码是可以运行的,按需也是 ok 的。
但是ssr无效了,查看网页源代码无内容。
动态路由 SSR 双端配置
ssr 无效了,这是什么缘故原由呢?
上面我们在做路由同构的时候,双端利用的是同一个 route配置文件 routes-config.js ,现在组件改成了按需加载,以是在路由查找后得到的组件发生改变了 - AyncDetail,AyncIndex ,根本无法转换出组件内容。
ssr 模式下 server 端如何处理路由按需加载
实在很大略,也是参考客户真个处理办法,对路由配置进行二次处理。server 端在进行组件查找前,逼迫实行 import 方法,得到一个全新的静态路由表,再去进行组件的查找。
如今我们离目标更近了一步, server 端已兼容了按需路由的查找。但是还没完!
我们这个时候访问页面的话,ssr 生效了,查看网页源代码可以看到对应的 html 内容。
但是页面上会显示直出的内容,然后显示 <span>正在加载......</span> ,瞬间又变成直出的内容。
ssr 模式下 client 端如何处理路由按需加载
这个是为什么呢?
是不是看的有点累了,再坚持一下就成功了。
实在有问题才是最好的学习办法,问题办理了,路就通了。
首先我们知道浏览器端会对已有的节点进行双端比拟,如果比拟失落败就会重新渲染,这很明显便是个问题。
咱剖析一下,首先做事端直出了 html 内容,而此时浏览器端js实行完后须要做按需加载,在按需加载前的组件默认的内容便是 <span>正在加载......</span> 这个缺省内容和做事端直出的 html 内容完备不同,以是比拟失落败,页面会渲染成 <span>正在加载......</span> ,然后按需加载完成后组件再次渲染,此时渲染的便是真正的组件了。
如何办理呢?
实在也并不繁芜,只是不愿定是否可行,试过就知道。
既然客户端须要处理按需,那么我们等这个按需组件加载完后再进行渲染是不是就可以了呢?
答案是:可以的!
如何按需呢?
向“做事端同学”学习,找到对应的组件并逼迫 实行 import 按需,只是这里不是转换为静态路由,只找到按需的组件完成动态加载即可。
既然有了思路,那就撸起代码。
matchComponent 是我封装的一个组件查找的方法,在文章开始已经先容过类似的实现,代码就不贴了。
核心亮点说完,全体流程基本结束,剩下的都是些有的没的了,我打算要收工了。
其他
SEO 支持
页面的 SEO 效果取决于页面的主体内容和页面的 TDK(标题 title,描述 description,关键词 keyword)以及关键词的分布和密度,现在我们实现了 ssr 以是页面的主体内容有了,那如何设置页面的标题并且让每个页面(路由)的标题都不同呢?
只要我们每要求一个路由的时候返回不同的 tdk 就可以了。
这里我在所对应组件数据预取的方法内加了约定,返回的数据为固定格式,必须包含 page工具 ,page 工具内包含 tdk 的信息。
看代码瞬间就明白。
这样你的 tdk 可以根据你的须要设置成静态还是从接口拿到的。然后可以在 esj 模板里进行绑定,也可以在 componentDidMount 通过 js document.title=this.state.page.tdk.title 设置页面的标题。
fetch 同构
可以利用 isomorphic-fetch 、 axios 或者 whatwg-fetch+node-fetch 等库来实现支持双真个 fetch数据要求 ,这里推举利用 axios 紧张是比较方便。
TODO 和 思考
没有先容结合 redux 状态管理的 ssr 实现,实在也不繁芜,关键还是看业务中是否须要利用redux,由于文中已经实现了利用 context 通报数据,直接改成按 store 通报也很随意马虎,但是更多的还是对 react-redux 的运用。
做事端同构渲染虽然可以提升首屏的涌现韶光,利于 SEO,对低端用户友好,但是开拓繁芜度有所提高,代码须要兼容双端运行(runtime),还有一些库只能在浏览器端运行,在做事端加载会直接报错,这种情形就须要进行做一些分外处理。
同时也会大大的增加做事端负载,当然这都随意马虎办理,可以改用 renderToNodeStream()方法通过流式输出来提升做事端渲染性能,可以进行监控和扩容,所以是否须要 ssr 模式,还要看详细的产品线和用户定位。
末了
本文最初从 react ssr 的整体实现事理上进行解释,然后逐步的抛出问题,循规蹈矩的逐步办理,终极完成了全体 ReactSSR 所须要处理的技能点,同时对每个技能点和问题做了详细的解释。
但实现办法并不唯一,还有很多其他的办法, 比如 next.js , umi.js ,但是事理相似,详细差异我会接下来进行比拟后输出。
源码参考
由于上面文中的代码较为零散,恐怕不能直接运行。为了方便大家的参考和学习,我把涉及到代码进行整理、完善和修正,增加了一些根本配置和工程化处理,目前已形成一个完全的开拓骨架,可以直接运行看效果,所有的代码都在这个骨架里,欢迎star 欢迎 下载,互换学习。
项目代码地址: https://github.com/Bigerfe/koa-react-ssr
说点感想
很多东西都可以基于你现有的知识创造出来。
只要明白了个中的事理,然后梳理出实现的思路,剩下的便是撸代码了,期间会大量的自动或被动的从你现有的知识库里进行调取,一步一步的,只要不怕麻烦,都能搞得定。
这也是我为什么上来先要说下 reac ssr事理 的缘故原由,由于它辅导了我的实践。
全文都是自己亲手一个一个码出,也全部都是出自本人的理解,但个人文采有限,以是导致很多表达说的都是大口语,表达不足清楚的地方还请指出和斧正,但是真正的核心已全部涵盖。
希望本文的内容对你有所帮助,也可以对得住我这个自傲的标题。