前端同构渲染的思考与实践

开篇

前端同构渲染的相关架构,给我最直观的感觉,这是前端渲染最为复杂的一种方案,也是为了追求极致的用户体验不得不去作的一种尝试,虽然 Node.js 的引入赋能了传统前端领域、SEO 优化也再也不是个问题,但很明显,这些只是副产品。html

问题

上帝为了咱们开了一扇窗,同时也会为咱们关上一扇门。前端

咱们所知的传统型 SPA,单页面应用,贴近用户端越近,交互越复杂,它的弊端就越明显,在咱们享受 JavaScirpt 给咱们带来的无刷新体验和组件化带来的开发效率的同时,『白屏』这个随着 SPA 各类优势随之而来的缺点被遗忘,咱们拥有菊花方案在 JavaScript 没有将 DOM 构建好以前蒙层,拥有白屏监控方案将真实用户数据上报改进,但并无触碰到白屏问题的本质,那就是『DOM 的构建者是 JavaScript,而非原生的浏览器』。vue

<html>
  <head><title /></head>
  <body>
  	<div id="root"></div>
    <script src="render.js"></script>
  </body>
</html>
复制代码

如上代码,在 SPA 架构中,服务器端直接给出形如这样的 HTML,浏览器在渲染 body#root 这个节点完成以后,页面的绘制区域其实仍是空的,直到 render.js 构建好真实的 DOM 结构以后再 append 到 #root上去。此时,首屏展现出来时,必然是 render.js 经过网络请求完毕,而后加上 JavaScript 执行完成以后的。node

让咱们回到最初的那个前端时代,那时候 JavaScript 尚未那么强大,咱们的服务器端所有吐出 HTML 给前端,咱们使用 jQuery 解决用户的交互,这种方式虽有不少弊病,但不能否认的是拥有理论上最低白屏时间。webpack

<html>
	<head><title /></head>
  <body>
  	<div id="root">
    	<div class="header">
        <img src="logo.png" />
      </div>
      <div calss="content">
        <div class="shopitem">
        </div>
      </div>
    </div>
  </body>
</html>
复制代码

如上代码,在直出的服务器渲染中,浏览器直接拿到最终的 HTML,浏览器经过解析 HTML 以后将 DOM 元素生成而进行渲染。因此相比于 SPA,服务器端渲染从直观上看:ios

  • 转化 HTML 到 DOM,浏览器原生会比 JavaScript 生成 DOM 的时间短
  • 省去了 SPA 中 JavaScript 的请求与编译时间

**web

解决

Node.js 的出现极大程度的给传统前端赋予了更大的能量,前端的分离也从前期的物理文件的区分转变为职责上的区分,前端开发者从页面仔的噩梦中解脱出来,最重要的是,JavaScript 能在服务器端执行了。在享受这些红利的同时,咱们就会不自觉的设想一种方案,它拥有 SPA 的大部分优势,却解决了它大部分的缺点,那就是服务器端输出 HTML,而后由客户端复用该 HTML,继续 SPA 模式,这样岂不是既解决了白屏和 SEO 问题,又继承了无刷新的用户体验和开发的组件化嘛。redis

嗯,若是这样的话,就会有个一致性的问题。咱们必须在浏览器端复用服务器端输出的 HTML 才能避免多套代码的适配,而传统的模板渲染是可行的,只要选择一套同时支持浏览器和 Node.js 的模板引擎就能搞定。咱们写好模板, 在 Node.js 准备好数据,而后将数据灌入模板产出 HTML,输出到浏览器以后由客户端 JavaScript 承载交互,搞定。axios

软件开发中遇到的全部问题,均可以经过增长一层抽象而得以解决后端

思路到了这里,咱们就会发现,『模板』实际上是一种抽象层,虽然底层的 HTML 只能跑在浏览器端,可是顶层的模板却能经过模板引擎同时跑在浏览器和服务器端,此为垂直方向,在水平方向上,模板将数据和结构解耦,将数据灌入结构,这种灌入,实际是一锤子买卖,管生无论养。

随着时间的推动,组件化的大潮来了,其核心概念 Virtual DOM 依其声明式和高性能让前端开发者大呼爽爽爽,但究其本质,就是为了解决频繁操做 DOM 而在 HTML 之上作的一层抽象,与模板不一样的是,它将数据与结构产生交互,有表明的要数 Facebook 方使用的单项数据流和 Vue 方使用的 MVVM 数据流,大道至简,咱们观察函数 UI = F(data), 其中 UI 为最终产出前端界面,data 为数据,F 则为模板结构或者 Virtual DOM,模板的方式是 F 只执行一遍,而组件方式则为每次 data 改变都会再执行一遍

因此理论上,不管是模板方式仍是组件方式,先后端同构的方案都呼之欲出,咱们在 Node.js 端获取数据 ,执行 F 函数,获得 HTML输出给浏览器,浏览器 JavaScript 复用 HTML,继续执行 F 函数,等到数据变化,继续执行 F 函数,交互也获得解决,完美~~~

实施

但因为组件化大势所趋,下文将略去模板方案,咱们以 Vue 为类比,下图代表其实施思路:

通用代码

因为 F 同时须要在浏览器端和服务器端执行,因此对于整个 Vue App,咱们须要同时支持两端,也就是通用代码。因此咱们须要将 SPA 架构的代码进行改造:

  • 分为两个入口,分为服务端和客户端,只引入通用代码,而后在不一样的环境里调用各自的渲染函数。固然,在客户端 ReactDOM.render 会生成 DOM 结构,而服务器端经过 ReactServer.renderToString 将生成 HTML,须要由 HTTP Server 推给前端,各入口处解决特异的环境问题;
  • 通用代码中不可在不断定执行环境的状况下引用 DOM、调用 window、document 这些浏览器特异和引用 global process 这些服务器端特异的操做,这每每是引发 Node.js 服务出问题的根本缘由;
  • 为了兼容两端,在选择库时,须要也同时须要支持两端,好比 axios,lodash 等;
  • React 和 Vue 都有生命周期,须要区分哪些生命周期是在浏览器中运行,哪些会在服务器端运行,或者是同时运行,如使用 Redux 或者 Vuex 等库,最好在组件上引入 asyncData 钩子进行数据请求,同时供两端使用;
  • 断定不一样的执行环境能够经过注入 process.env.EXEC_ENV 来解决,形如:
if (process.env.EXEC_ENV === 'client') {
  window.addEventListener(...);
}

if (process.env.EXEC_ENV === 'server') {
}
复制代码

构建与运行

  • 在使用 webpack 进行构建时,须要将公共 App 部分打包出来,造成公共代码,由服务器端引入执行,而客户端能够引用打包好的公共代码,再用 webpack 引入以后进行特异处理便可;
  • 须要引入 Node.js 中间层,负责请求数据,提供渲染能力,提供 HTTP 服务,因为 HTML 模板须要在服务端引入,CDN 文件须要自行处理;
  • 至于 babel 的使用,能够在浏览器中通用处理,服务端只解决特殊语法,如 jsx,vue template;

新世界

至此,白屏问题问题看起来是解决了,经过把 JavaScript 的渲染逻辑放到 Node.js 端进行,咱们加快了首屏出现的时间,可是联想到 Node.js 对前端的赋能,咱们或许能够作的更多。

再议首屏

让咱们把视角移动的更细致一些,关注『从服务器端输出 HTML』这一部分,其隐藏的含义是咱们须要把 App 渲染的全部 HTML 都输出给前端,其实否则,举个栗子:

好比在移动端有一个页面,它有大约 10 屏的高度,若是咱们在服务器端所有输出 10 屏实际上是有点浪费的,咱们能够只输出首屏须要的,从而下降 render 执行时间从而下降 TTFB 时间,让页面更快的到达用户眼前。实践中,通常状况是输出大概快两屏的样子,就能处理因此机型的高度问题,剩下的 8 屏,在浏览器端继续渲染,渐进产出内容,用户无感知。

资源控制

得益于 Node.js 输出 HTML 的另外一层含义,就是咱们能够直接在首次接触就能感知到客户端,也就有了足够的灵活性,再举个栗子:

有个针对安卓平台和 iOS 平台不一样的脚本只要加载,若是在 SPA 状况下,只有等 JavaScript 执行时咱们断定 navigation.userAgent 来获知先在是哪一个平台,而后在 appendChild 一个 script 到 body,但若是服务端能首次接触就能感知,咱们能够在服务端直接拿到 HTTP 请求中的 userAgent 断定平台,根据标识在模板中处理,很显然,这样很稳。

另外,若是有一些特别复杂的计算,服务端能够有更多的办法将数据更快的处理,以免繁忙无比的浏览器接手。

缓存控制

通常的业务场景下,咱们须要在 Node.js 中经过内网将数据获取到,而后经过 render 函数渲染出 HTML(通常须要将数据附带给 HTML 输出以便重复利用),这个时候咱们能够经过页面访问地址和生成的 HTML 字符串作缓存策略,在缓存(通常选择 redis 等方案)以后,下次直接将一样的页面直接输出到前端,可大幅提升渲染性能

但这种方案也有不少限制,由于要考虑页面地址、多平台下、帐户是否登陆,页面是否须要改动等状况:

  • 页面地址纬度,在不一样的地址下,HTML 输出不一致,因此 URL 可做为 key 的元素之一;
  • 未登陆态,页面能够直接缓存,如需断定平台特异,需在 Node.js 端进行处理;
  • 已登陆态,若是已缓存某一个已登陆用户的 HTML,须要将跟登陆相关的组件抹去从新换掉,或者直接给予未登陆态页面,在客户端进行变动。

挑战

同构渲染看似美好,但其相对传统 SPA 确有着更多挑战:

Node.js

服务器端渲染相对应传统的 Node.js 应用,renderToString 函数不只 CPU 密集,并且不一样的组件对机器资源的要求不尽相同,这就更须要 Node.js 指标的监控、日志的记录、错误的收集、崩溃机制的完善。这里额外的关键的指标是 renderToString 的时间,它反应了 Node.js 渲染所使用的时间,若是加入缓存机制,就须要统计命中率等等。

代码质量

关于写通用代码,要求比 SPA 架构对开发者提出了更高的要求,咱们须要当心再当心,由于万一搞错,将致使很难排查的内存泄露和 CPU 飙升,而且一旦出了问题,就像要修理天上跑的飞机同样,很是困难。还记得有一次在相似 componentWillMount 写了一些跟浏览器相关的代码致使的内存飙升,还有一次 JSON.stringify 一个大对象致使的 CPU 飙升,不堪回首。这方面 alinode 作的很好,确实能够知足这种飞机场景。

结语

为了效率, 前端们付出了艰辛的努力,不管是工程上咱们想方设法的制造工具,仍是组件化的引入,咱们解决的是开发的效率,而不管是 Virtual DOM 的引入解决频繁操做的 DOM,仍是用了提高用户体验而使用的 SPA 架构,咱们解决的是用户的使用效率,是前端的性能。而同构渲染也是这样一种方案,它引入了 Node.js 的复杂度,要求咱们写出限制更多的代码,其根本目的仍是为了让用户更快更早的看到页面,那怕是 50 毫秒,那怕是 10 毫秒。


关于咱们

咱们是蚂蚁保险体验技术团队,来自蚂蚁金服保险事业群。咱们是一个年轻的团队(没有历史技术栈包袱),目前平均年龄92年(去除一个最高分8x年-团队leader,去除一个最低分97年-实习小老弟)。咱们支持了阿里集团几乎全部的保险业务。18年咱们产出的相互宝轰动保险界,19年咱们更有多个重量级项目筹备动员中。现伴随着事业群的高速发展,团队也在迅速扩张,欢迎各位前端高手加入咱们~

咱们但愿你是:技术上基础扎实、某领域深刻(Node/互动营销/数据可视化等);学习上善于沉淀、持续学习;性格上乐观开朗、活泼外向。

若有兴趣加入咱们,欢迎发送简历至邮箱:luguang.ylg@antfin.com


本文做者:蚂蚁保险-体验技术组-月影

掘金地址:杨柳岸酱

相关文章
相关标签/搜索