前言
在前端页面的渲染这块,近几年来感觉经历了从服务端渲染(多页面)=>客户端渲染(SPA)=>同构渲染的演变,同构渲染是15年左右React火了一段时间后提出的概念(也差不多是《三体》那个时候),原文是 Isomorphic rendering,但是很快就有人驳斥不应该用 Isomorphic 这个词,而应该用 Universal Rendering。
提前列举一下术语:
1、前端渲染:浏览器一侧使用js,借助模版或vue、react、angular等框架做的DOM结构生成(首屏展示出来时,必然是 render.js 通过网络请求完毕,然后加上 JavaScript 执行完成之后的。)
2、后端渲染:服务器一侧,使用php、jsp、velocity、nodejs等技术实现DOM结构生成,并在HTTP请求中返回给浏览器(好处是转化 HTML 到 DOM,浏览器原生会比 JavaScript 生成 DOM 的时间短;省去了 SPA 中 JavaScript 的请求与编译执行时间)
3、同构渲染:简单来说就是一份代码,服务端先通过服务端渲染(server-side rendering,下称ssr),生成html以及初始化数据,客户端拿到html和初始化数据后,通过对html的dom进行patch和事件绑定对dom进行客户端激活(client-side hydration,下称csh),这个整体的过程叫同构渲染。其实就是满足三个条件:1)同一份代码 2)ssr 3)csh
Q. 问题来了,同构渲染和php,jsp的渲染有什么不同,同构渲染称得上ssr吗?
既然同构渲染就是一份代码ssr加csh,那么ssr部分本质上跟php,jsp也是一样,也就是模版引擎出来html字符串,但是同构要求能通过一份代码实现ssr,csh,同时服务端需要提供一份初始化数据以便csh,而且传统php,jsp不关注csh过程。
Q. 同构渲染能解决首屏加载和seo的问题吗?
其实解决这两个问题,是ssr来解决的,csh解决的只是交互问题。既然文件已经交由服务端渲染完,展示的速度肯定是提升的(因为不需要等待客户端渲染),同样seo蜘蛛一般是分析服务端出来的html的,那么没有ssr的spa客户端等同于无法seo(当然一些蜘蛛也会提供spa的方案),同构包括ssr,当然可以解决seo的问题
但解决归解决,不一定能很好解决。毕竟这几个其实不是同样的问题。就像我个人觉得ssr来实现seo本身就是个伪需求,同理首屏加载问题也是。
seo其实还能通过预渲染或者通过ua判断让蜘蛛走headless chrome渲染来解决。另外首屏加载速度问题的解决方案就更多了。
比较前后端还是属于不同环境,同构考验开发人员对大局观的考虑,约束了特定环境的一些开发灵活性,同时也影响代码解耦。ssr抛开这两点好处,甚至没有找到任何优点。所以这是当前前后端同构不温不火的重要原因
同构的历史背景
- Web开发的历程是很有趣的,最初php、asp的年代,一切内容都是服务器渲染的;
- 后来为了节省服务器资源,也更大限度利用客户端缓存,又出现了前后端分离的模式,从而有了专业的前端开发和后台开发。此时Web的特点是,js和html放到静态目录或者CDN,并以ajax方式获取后台的数据,在前端进行DOM组装。这种开发方式沿用至今,这是一个好的工作模式,专业的人做专业的事,确实有利于工作效率提高。引领SPA时代到来,但是JavaScirpt给我们带来的无刷新体验和组件化带来的开发效率的同时,白屏和SEO问题也随之而来。
- 随着nodejs的流行,前端jser们又开始蠢蠢欲动,尝试吞并web接入这个后台的前沿地盘,把后台推到更后。大概2014年后,又出现了很多nodejs直出的方案,把页面数据都一次在HTML的请求中返回,无需浏览器端再发起ajax获取数据,而且服务器端把DOM结构都渲染好,拿到初始化数据给浏览器,浏览器得到html和初始数据进行patch和事件绑定对客户端激活。不得不说,这个方案带来了很多好处:既解决了白屏和 SEO 问题,又继承了SPA无刷新的用户体验和开发的组件化带来的效率。首屏速度更快,浏览器更省电。当然,随之而来的,就是更复杂的工作模式,jser需要做服务器端的逻辑,甚至一些代码需要同时用在浏览器和nodejs上。同构应运而生。

百度搜一下前后端同构,清一色的vue、react。这些确实是同构,但我认为范围太窄,同构不是框架带来的问题,而是因为前后端独立渲染这种架构层面带来的问题
SSR核心原理
整体来说服务端渲染原理不复杂,其中最核心的内容就是同构。
1)node server 接收客户端请求,得到当前的req url path
2)然后在已有的路由表内查找到对应的组件,拿到需要请求的数据,将数据作为 props
、context或者store 形式传入组件
3)然后基于react内置的服务端渲染api renderToString() or renderToNodeStream() 把组件渲染为 html字符串或者 stream 流, 在把最终的html进行输出前需要将数据注入到浏览器端(注水),
4)server 输出(response)后浏览器端可以得到数据(脱水),浏览器开始进行渲染和节点对比,然后执行组件的componentDidMount完成组件内事件绑定和一些交互,浏览器重用了服务端输出的 html 节点,整个流程结束。

同构之道
如何从实际项目需求的角度来看,找出自己所需的同构之道
同构,是为了提高用户体验的同时,提高团队的工作效率。接下来,我想根据项目的类型,说说自己的看法。
第一种,单页面应用
这个网站很类似一个APP,确实很有必要做成单页应用,有助于提高用户体验。
如果第一步选择了单页面应用,这里就衍生了另外的问题——SEO。而react等框架做了服务器渲染,最大目的其实也是解决SEO。
既然浏览器端选择了某个框架,例如React,而同时又考虑nodejs直出提高首屏的速度,那么就没有讨价还价的余地了,当然上react全家桶,前后端都用react。
第二种,多页面纯数据展示,每一页都比较简单,没有分屏的必要
如果项目是这样的情况,使用nodejs直出,无非就是提高打开速度。而前后端基本八竿子打不着,最多就是一些工具函数(转换一下日期格式,输入框校验)要做复用。
此时,没必要大费周折去考虑什么框架,因地制宜,想想自己需要什么即可。
要解决函数库的前后端复用,可以简单做commonjs/window的兼容。
如果浏览器端的代码比较多,就可以考虑粒度化,使用webpack做浏览器端代码打包,同时commonjs的写法也可以复用到nodejs层。
第三种,多页面而且每一页不是那么简单,首屏和次屏有一些HTML片段(模版)需要复用
用webpack做前端打包,这样前端各种代码和后台代码都是commonjs风格,可以二合一。而且发布前打包为一个大js文件,也省去nodejs每次请求动态合并js的消耗。
html模版发布前先做预编译,从html+模版语法,转为纯js代码,随着webpack打包到浏览器端大js文件中。
后端和前端都用到的代码,基于commonjs,尽可能的抽离封装。
最终合并的浏览器端大js还是动态合并到首屏HTML中。
引入了webpack到浏览器端,就能很好的解决了原先html模版传播的尴尬。而模块风格的统一,也有利于前后端代码更好的复用。
至于最终浏览器js是否打包到首屏HTML中,还是单独的部署CDN,这个其实就不是同构的问题了。不过对于移动端而言,还是建议合并在一起。
抽象一下,对于第三种项目情况,跳出我原先的项目。我认为,关键是要把前后端使用的模版统一为一个方式引入。
第四种,还是多页面,浏览器端没有模版拼装的需求,第三种情况的变种
或者说,这个不是一个单独的项目情况,只是因为用的技术方案不同。跟第三种情况一样,但次屏的渲染,我们不在浏览器端执行,而是继续交给nodejs。浏览器端通过ajax把次屏html片段拉取回来,然后直接塞到body中。而且,除此之外,浏览器端没有用户交互会导致已有的DOM发生重绘,或者极少内容重绘,不需要动用到模版。
在这个情况下,浏览器端js更纯粹的只关注事件处理。
我觉得这个又回到了第二种情况,只需要简单把一些库函数封装一下,做成前后端共用即可。
第四种情况,因为彻底抛弃了浏览器渲染,整个情况就简单多了,不需要考虑模版和很多逻辑js的前后端复用问题。
思考
上帝为了我们开了一扇窗,同时也会为我们关上一扇门。
我们所知的传统型 SPA,单页面应用,贴近用户端越近,交互越复杂,它的弊端就越明显,在我们享受 JavaScirpt 给我们带来的无刷新体验和组件化带来的开发效率的同时,『白屏』这个随着 SPA 各种优点随之而来的缺点被遗忘,我们拥有菊花方案在 JavaScript 没有将 DOM 构建好之前蒙层,或者是骨架屏方案让用户提前感知页面结构,拥有白屏监控方案将真实用户数据上报改进,但并没有触碰到白屏问题的本质,那就是『DOM 的构建者是 JavaScript,而非原生的浏览器』。
同构的到来既解决了白屏和 SEO 问题,又继承了SPA无刷新的用户体验和开发的组件化带来的效率。但是面临更大的挑战。
参考:
https://juejin.im/post/5c821dc45188257e1f2915b1
https://segmentfault.com/a/1190000016722457
https://developers.google.com/web/fundamentals/performance/user-centric-performance-metrics?hl=zh-cn
https://juejin.im/post/5d7deef6e51d453bb13b66cd#heading-5