《Building Isomorphic JavaScript Apps》 翻译笔记(1)——定义

Posted by Zhangjd on March 14, 2017

《同构JavaScript应用》(原书Building Isomorphic JavaScript Apps:From Concept to Implementation to Real-World Solutions)是我翻译的第二本书,整本书分为三个部分:介绍与关键概念、构建一个同构应用(附带代码示例)、现实解决方案。总的来说,整本书除了 PART II 的代码示例部分小瑕疵比较多,感受还是比较有指导意义的。我将会分开几篇笔记,来总结书中的关键观点。

第一篇笔记,介绍同构 JavaScript 的定义,并回顾了 Web 发展历史并对比其他 Web 应用架构方案,通过 SEO 支持、优化首屏加载速度、优化页面过渡效果这三个关键的验收标准评估这些架构。把传统 Web 应用和单页面应用的优势结合起来,得到了同构 JavaScript 应用架构。最后简要分析了不需要使用同构的情况。

先抛结论:

  1. 传统 Web 应用:服务器端控制页面渲染,SEO 和首屏加载速度有优势;网页切换体验不好(整页跳转);引入 Ajax 可以解决体验问题,但是会使得代码难以跟踪和维护(前后端都可以控制渲染,导致职责模糊)
  2. 单页面应用:客户端控制页面渲染,页面切换体验良好;减少服务器端压力;SEO 支持度堪忧(需要较高代价);首屏加载速度慢(js bundle 文件大、需要经过 js 解析才能看到渲染内容)
  3. 同构应用:综合以上两种的优点;架构上需要更多的考虑,代码复杂度高;

1.1 定义同构 JavaScript

同构 JavaScript 应用就是在浏览器客户端以及 Web 应用服务器端共享同一套 JavaScript 代码的应用。之所以称为同构,是因为从某种意义上讲,无论应用运行在客户端还是服务器端,都具有相同的形式或形态。

Isomorphic JavaScript 这一术语公认的出处是 Charlie Robbins 在 2011 年发表的一篇名为《Scaling Isomorphic Javascript Code》的博客文章。随后,这个术语在 Spike Brehm 2013年发表的文章《Isomorphic JavaScript: The Future of Web Apps》以及随后的一些文章和会议演讲再次出现,因此开始流行起来。然而在 JavaScript 社区中,关于同构的用词 “isomorphic” 也有一些争论。Michael Jackson(React.js 讲师、react-router 项目共同作者之一)认为,应该把同构 JavaScript 称为universal JavaScript。Jackson 认为 “universal” 这个词可以突出“JavaScript 不仅可以在服务器端和客户端上运行,还可以在原生设备和嵌入式架构上运行”的特点。

“Isomorphism” 源于一个数学术语:对于两个数学对象,如果我们简单地忽略掉对象的个体差异,当它们具有相似的属性和操作时,这两个对象就是同构的。

1.2 我们为什么需要同构 JavaScript?

要理解同构 JavaScript 方案的由来,我们必须先了解这个方案出现时的状况。

(作者就职于沃尔玛实验室,主要从电商应用的角度考虑问题)考虑到我们的主要业务场景是电商应用,我们来看看在Web发展史中适用于该场景的几种不同的架构。先要确定一些关键的评判标准,以评价同构以及其他的Web应用架构方案。在整个评估过程中,必须把这些业务标准和工程的主要关注点(可维护性和效率)进行权衡:

  1. 应用应该能够被搜索引擎收录。
  2. 应用的首屏加载速度应该是经过优化的——也就是说,关键渲染路径(critical rendering path)应该属于初始响应中的一部分。
  3. 应用应该能够响应用户的交互操作(比如优化网页切换)。

关键渲染路径指的是页面上与用户的主要操作相关的内容。在一个电子商务应用中,关键渲染路径可以是一个商品的描述。至于新闻网站则可以是一篇文章的内容。

作者把架构分成了三种:传统 Web 应用、单页应用、同构应用。

1.2.1 传统 Web 应用

传统Web应用指的是:所有的标签(或者至少是关键渲染路径包含的标签)是通过服务器使用某种服务端语言进行渲染的,比如 PHP、Ruby、Java 等(如图所示)。浏览器解析文档后,用于丰富用户体验的 JavaScript 代码会被初始化。

图:传统 Web 应用流程

首先,它很容易被搜索引擎收录,因为当爬虫遍历应用时可以爬取所有的内容,所以消费者是可以搜索到应用内容的。其次,页面加载也经过了优化,因为关键渲染路径的标签通过服务器端进行渲染,从而提高了感知渲染速度,降低了用户跳出应用的可能。然而,传统Web应用只能满足上述三点要求之中的两点

“感知渲染速度(“perceived” rendering speed)”是什么意思?Ilya Grigorik 在《Web 性能权威指南》一书中是这样解释的:“时间测量是客观的,而时间感知是主观的。我们可以通过设计来改善感知性能。”(“Time is measured objectively but perceived subjectively, and experiences can be engineered to improve perceived performance.”)

在传统 Web 应用中,页面导航和数据传输的工作都遵循 Web 原本设计的方式进行。当用户导航到一个新页面,或者提交表单数据,又或者是页面信息发生改变时,浏览器发送请求、取得响应并解析完整的文档流。这种方式在实现前两个评判标准时极为有效,然而这种全页面安装拆卸的生命周期代价非常高,因此在响应性方面这仅仅是一个次优解。既然我们有幸生活在拥有 Ajax 的年代,我们都已经知道这是一种比整页刷新更加高效的方法,但 Ajax 的引入也会带来成本,我们将会在下一节进行探讨。可是在过渡到下一节之前,我们应该先看看在传统 Web 应用的背景下是如何应用 Ajax 的。

图:使用 Ajax 的传统 Web 应用流程

一个(相关)商品轮播组件可以分页浏览产品。在某些情况下所有产品都是预先加载的,但有时候会因为商品数量太多而不能采用预加载。在第二种情况下,需要发起网络请求获取下一页的商品信息。由于刷新整个页面的效率极低,因此典型的解决方案是在翻页时使用 Ajax 获取新的商品页面集。接下来可优化的地方是只返回渲染页面集所需要的数据,这意味着你需要创建用于重复生成的模板、模型和静态资源,并在客户端进行渲染(如图所示)。这种做法需要编写更多单元测试。这个例子非常简单,但如果你把这种思想推广到大型应用中,你会发现应用将会变得难以跟踪与维护——因为你不能轻易地推断出应用是如何结束在某个特定状态的。此外,重复编写渲染逻辑是一种资源的浪费,而且在添加或者修改功能时,同时操作两份 UI 代码会导致应用出现 bug 的概率更高。

1.2.2 单页面应用

SPA把渲染工作完全转移到客户端进行,解决了一直以来困扰着传统 Web 应用的问题。该模型把应用逻辑从数据检索中抽离出来,并把界面代码整合为单一语言和运行时,因此可以有效减少服务器端的压力(如图所示)。

图:单页应用流程

之所以能做到减少服务器端压力,是因为服务器首先把一份包含了静态资源、JavaScript 和模板的静荷数据(payload)发送到客户端。之后,客户端只需要获取渲染页面或视图所需要的数据即可。这种行为显著提高了页面的渲染效果,因为避免了用户请求新页面或者提交数据时重新请求并解析页面的性能开销。除了性能收益外,这种模型还解决了 Ajax 引入到传统 Web 应用时产生的工程问题

在单页面应用中,呈现给最终用户的初始页面加载速度可能会非常缓慢,因为用户必须等到数据请求完成才能看见页面渲染。因此,用户最多只能在页面加载时看到加载指示器动画,而不能立即看到内容。针对渲染延迟的问题,有一种常见的折中方案是为初始页面的数据提供专门的数据优化服务。但这样做需要编写额外的服务端应用逻辑,从而导致两端职责范围再次变得模糊,还需要额外维护另外一层代码。

SPA 面临的第二个问题关系到用户体验和企业利益。在默认情况下,SPA 对于 SEO 不友好,这意味着用户不能通过搜索引擎找到应用相关的内容。这个问题源于 SPA 利用了 hash 片段实现路由。在我们分析为什么这种方式会影响 SEO 之前,我们先看看 SPA 路由的机制。

SPA 依赖 hash 片段把人造的 URI 路径映射到路由处理器中,并渲染对应的视图。举个例子,在传统 Web 应用中,“关于我们”的页面 URI 可能是 http://domain.com/about,但在SPA中则可能是 http://domain.com/#about。SPA 在 URL 的末尾添加了一个“#”号和一个片段标识符。SPA 路由之所以要利用 hash 片段,是因为片段的内容发生变化时,浏览器不会像 URI 发生变化时那样发起新的网络请求。这点至关重要,因为 SPA 的整个大前提就是只请求页面或视图渲染所需要的数据,而不是为每一个页面获取并解析整份文档。

SPA 片段对 SEO 不友好的原因是:hash 片段不会作为 HTTP 请求中的一部分发送给服务器(按照规范定义)。对于 Web 爬虫而言,http://domain.com/#abouthttp://domain.com/#faqs 是同一个页面。幸运的是,Google 定义了一种变通方案,为 hash 片段提供了SEO支持:使用“#!”(hashbang)。

很多SPA库目前已经支持 History API,并且最近谷歌的爬虫对于索引 JavaScript 应用提供了更好的支持——在之前,JavaScript 代码甚至不会被 Web 爬虫所执行。

按照谷歌的规定,其基本前提是把 SPA 路由片段中的“#”替换为“#!”,因此 http://domain.com/#about 需要改为 http://domain.com/#!about。这样谷歌的爬虫才能确定这个页面的内容需要被索引,而不仅仅是简单的锚点。

随后,爬虫把这个链接转换为完全合格的 URI 版本,因此 http://domain.com/#about 会变成 http://domain.com/?query&_escaped_fragment=about。在那时,服务器端需把负责把 SPA 页面对应的的屏幕快照提供给爬虫。

在这时,SPA的价值主张开始愈发下降了。从工程角度上讲,需要在下列方案中二选一:

  1. 在服务器中运行一个无界面的浏览器(headless browser),比如 PhantomJS,用于在服务器中运行SPA并响应爬虫请求。
  2. 把这个问题外包给第三方供应商解决,比如 BromBone

这两种修复方案都需要成本,而前面提及的首屏渲染不理想的成本还没有包含在内。幸运的是,工程师都善于解决问题。正如从传统 Web 应用到 SPA 的改进那样,下一代的架构又诞生了,也就是同构 JavaScript。

1.2.3 同构应用

同构 JavaScript 应用是传统 Web 应用和单页面应用架构的完美结合。同构应用提供了:

  • 默认使用完全合法的 URL,支持 SEO——不再需要“#!”的变通方案了——通过 History API 进行跳转,在不支持 History API 的浏览器中可以优雅地回退到服务器端渲染模式
  • 在支持 History API 的浏览器中,后续请求使用了 SPA 模型的分布式渲染。这种实现同时可以减轻服务器负载
  • 对于同一个渲染周期,客户端和服务端可以重用同一套代码。这意味着我们不需要重复劳动,也不会让界限变得模糊。这可以在降低界面开发成本与bug数量的同时提高团队开发速度。
  • 通过在服务器端渲染首屏页面提高加载速度。用户不再需要在首屏渲染之前等待网络请求完成和一直看着加载指示器动画了。
  • 纯 JavaScript 技术栈,这意味着应用界面的代码可以由前端工程师单独维护,而无需经过后端工程师。更清晰的关注点和责任分离,使得每个人都可以只在自己擅长的领域贡献代码,这就是术业有专攻。

同构 JavaScript 架构可以同时满足本节开始提到的三个评判标准。同构 JavaScript 应用可以轻松地被搜索引擎收录,优化网页加载速度,并优化页面之间的过渡(适用于支持 History API 的浏览器,而在老版本浏览器中可以优雅降级,对于应用架构不会产生影响)。

1.3 什么时候不使用同构

举一些例子,像 Yahoo!、Facebook、Netflix 和 Airbnb 这些公司已经拥抱了同构 JavaScript。然而,同构 JavaScript 架构可能仅仅适用于某些类型的应用场景。正如我们将在本书中探索的那样,同构 JavaScript 应用需要更多架构上的考虑,同时实现上也存在一定的复杂度。对于单页面应用来说,如果性能要求不高或者没有 SEO 的需求(比如需要登录后才能使用),同构 JavaScript 带来的麻烦似乎远大于其收益。

而且许多公司和组织可能并不打算在服务器上操作与维护一个 JavaScript 的执行引擎。比如,Java、Ruby、Python、PHP 的开发者们可能并不知道如何在生产环境中对一个 JavaScript 应用服务器(比如Node.js)进行监控与故障诊断。在这些情况下,同构 JavaScript 可能会引起额外的操作成本,不好上手。

Node.js 提供了一个出色的服务器端 JavaScript 运行环境。对于目前使用了 Java、Ruby、Python 或者 PHP 的服务器,有两种主要的候选方案:1)额外运行一个 Node.js 进程,作为本地或者远程的“渲染服务”;2)使用嵌入式 JavaScript 引擎(比如集成在 Java8 中的Nashorn)。然而这两种方案都有明显的缺点。运行Node.js作为渲染服务需要在进行socket通讯时序列化数据,这带来了额外的开销成本。同样地,在其他语言中使用的嵌入式 JavaScript 引擎通常是没有经过优化的,可能会导致额外的性能问题(尽管这会随着时间推移得到改善)。

如果你的项目或者公司不需要借助同构 JavaScript 架构提供的便利(如本章所述),那当然没有问题,只要针对应用场景选择合适的技术即可。然而,当服务器端渲染不在你的选择范围,而你又需要关心首屏加载速度和搜索引擎优化的时候,别担心,这本书可以帮到你。