《同构JavaScript应用开发》读书笔记

当持续迭代更新一个产品的时候,会遇见很多的问题。其中,技术选型决定了整个项目的模板渲染方案。在过去的项目中,尝试了如下几种模板渲染:

  • 服务端渲染,Ajax刷新局部页面
  • 客户端渲染,采用Vue等框架构建单页面应用
  • 纯静态资源,采用Hexo的框架在本地生成静态页面,然后进行部署
  • SSR,基于Vue-SSR和express等框架完成的单页面应用服务端渲染

根据应用场景,每种方案均有优劣。恰好最近决定继续倒腾博客,正在阅读《同构JavaScript应用开发》这本书,稍作记录与思考。

<!—more—>

1. 几种渲染方案的问题

回到开篇的渲染方案,现在列出他们存在的一些问题。(感觉这就是我的博客的更新史啊~)

1.1. 服务端渲染

这是最原始的渲染方式,通过服务端输出经过数据填充的页面模板。页面上需要通过局部刷新的区域,通过ajax异步请求数据,然后操作DOM修改对应的内容。

这里ajax请求的数据可以有两种返回形式,

  • 继续通过后端模板引擎渲染数据,并返回html内容
  • 只返回json数据,通过前端解析并生成对应的html内容

如果采用第一种形式,不可避免会增加服务端压力以及网络传输成本(比如之前接触到的J-UI后台管理框架)。

如果采用第二种形式,相当于我们需要管理两份模板,一份是最初渲染页面时后端需要的模板,一份是异步更新时前端需要的模板。而后端和前端的模板引擎很可能是完全不同的语法和实现逻辑(比如bladeejs什么的)。毫无疑问,管理两份模板,修改和维护的成本都会成倍增加。

1.2. 客户端渲染

在SPA中,前后端分离会大大提升效率。API服务器只负责返回数据,应用服务器提供原始的静态页面数据(在vue-cli中对应的就是dist文件夹下面的资源),客户端负责展示数据。SPA应用中我们只需要维护一份模板,并且MVVM模式可以很方便地维护数据和模板。

但是单页面应用存在两个问题:

一是所有的模板和逻辑代码都放在js文件中进行,如果项目过大,则初始页面的加载速度可能会比较缓慢(因为打包后的静态资源体积可能会过于庞大)。利用CDN和组件按需打包可能可以缓解这个问题

第二个问题就是SEO的问题了。对于这个问题我深有体会,我司官网最初是采用AngularJS实现的,导致无法被百度收入,百度快照两个月才会更新一次,且内容全部为空。这是因为传统的爬虫只收录页面内容,而不会执行JavaScript代码。(貌似谷歌的爬虫现在可以了,但是国内...)

1.3. SSR

对于SSR这块并不是十分了解,之前采用Nuxt对整个博客进行了SSR更新,同样参考官网这句话

将同一个组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将静态标记"混合"为客户端上完全交互的应用程序

似乎解决了SEO的问题,但是SSR会增加服务器的压力,因为需要将虚拟DOM的vnode渲染成实际的html代码。此外,还包括学习成本过高、颗粒化SEO等问题。

1.4. 纯静态资源

用来搭建静态博客是个很好的选择,但是在实际的产品开发中,这种实现方式明显是不科学的~

1.5. 同构JavaScript

通过JavaScript是传统web应用和SPA的结合,可以上述的一些问题:

  • 支持SEO与路径优化
  • 完全的JavaScript开发环境,只需要维护一套模板
  • 通过服务端渲染优化首屏加载速度

当然,同构JavaScript也不是万能的,所以这就是学习的乐趣啊...

2. 同构相关的概念

同构指的是需要实现某种映射,能够将客户端的功能映射到服务端的环境中,反之亦然。

根据客户端和服务端共享代码的程度:

  • 最低程度的同构,通过共享模板的方式共享视图层,并共用辅助函数
  • 最大程度的同构,共享整个应用,包括视图层、模型、路由和控制器等
  • 介于二者之间的共享

2.1. 共享视图

共享视图意味着模板和相应的视图逻辑都需要共享。

客户端的模板渲染只需要对模板进行数据填充;而在服务端,同一个模板会被渲染成字符串并作为响应结果返回,客户端需要接手完成服务端未完成的工作。

而视图逻辑无非就是对应的JavaScript代码,只要与执行环境无关,就可以实现对应的逻辑共享。

2.2. 共享路由

在同构应用中,我们需要一套路由配置,并且能够在服务端和客户端之间进行共享。由于路由需要访问与环境相关的API,导致共享路由需要执行某些处理机制(比如在服务端需要获取当前的url并将其映射到对应的控制器方法上,而在客户端需要使用浏览器的API)

2.3. 共享模型

模型为数据建立了一种抽象,同构应用可以使用服务端返回首屏响应之前一模一样的状态,对客户端进行初始化。

(PS:表示共享模型并不是十分理解,总体来说就是确保数据的一致性?)

2.4. API的shim

shim这个单词在RequireJS中并不陌生。依赖于特定环境的API,我们可以通过环境判断抽象出一个统一的API,从而保证代码在两端运行,比如

if (typeof window == 'undefined'){
  ...
}else {
  ...
}

而对于与环境无关的公共模块,我们可以通过webpack对代码进行打包,从而将对应的Node模块转换到浏览器中。

3. 实现

书中的思路是先实现服务端的请求,然后将相关的代码共享到客户端。关于具体的实现部分我打算在完成博客的重构之后单独整理一篇文章,因此下面只是简单梳理一下思路,至于相关的环境安装这里就不提了~。

github项目地址传送门。说实话这跟我预期的同构实现方案有很大出入(也许是我之前的设想并不能算作是同构吧)

3.1. 服务端

由于写了不少后台代码,对于MVC框架的基本思路还是有一点了解的:定义路由并绑定对应的控制器方法,根据参数渲染模板最终返回给客户端完整的HTML代码。

3.2. 客户端

为了在客户端执行请求、响应周期,基本思路是劫持点击操作,然后异步请求数据并渲染模板。为了保持浏览器历史记录的完整性,我们可以使用HTML5的History接口来实现,这在之前的history与单页面应用路由已经整理过了。

3.3. 抽象

前后端均需要操作的对象包括cookie、重定向等,书中对这两个进行了示范,保证了前后端可以统一的接口。

4. 其他同构方案

本书的第三部分介绍了已经用于生产环境的同构解决方案,包括使用React、全栈AngularJS等,后面再做了解。

5. 小结

这本书我看开头觉得十分精彩,因为作者关于模板提出的几个问题我都在实际项目中遇到过,并且在此之前并没有很好的解决方案。也许同构方案在我目前的工作项目中也不一定能用的上,因为将后端逻辑从PHP迁往NodeJS成本太大了。

不过多了解一点东西也不错嘛~当下的任务是先实现一个同构项目,也就是先把博客实现同构...虽然不知道最后做出来的到底算不算是同构呢~

博客同构渲染实践 npm与一次版本更新事故