同构 JavaScript 应用开发

内容简介

本书将向你展示如何构建和维护属于自己的同构 JavaScript 应用。全书分为三部分,第一部分描绘不同种类的同构 JavaScript 的轮廓,第二部分介绍关键概念,第三部分提供业界同行的解决方案案例。通过阅读本书,你将了解到这种应用架构日益流行的原因,并将其运用于解决关键的业务问题,如页面加载速度和 SEO 兼容性。本书适合对同构 JavaScript 感兴趣的 Web 开发人员。

作者简介

Jason Strimpel,软件工程师,拥有十余年 Web 开发经验。目前任职于沃尔玛实验室,负责支持 UI 应用的软件开发。

Maxime Najim,沃尔玛实验室软件架构师,全栈 Web 开发者。曾任职于 Netflix、苹果和 Yahoo! 等公司,在创建大型、伸缩性强、可靠的 Web 应用方面具有丰富经验。

本书内容

前言

Jason Strimpel

我在多年前就开始了 Web 开发的职业生涯,当时我在加州大学圣地亚哥分校担任行政助理。我的工作职责之一就是维护部门网站。那个年代的 Web 开发还是站点管理员、表格式布局和 CGI 程序的天下,且网景浏览器仍然是浏览器领域中的佼佼者。当时,我的专业知识还有很多不足,也缺乏经验。我清晰地记得,当时我很担心,如果我发送的电子邮件中存在拼写错误,收件人就会看到它们被红色下划线标记出来,正如我看到他们的拼写错误时一样。幸运的是,我的上司很耐心,他说我的邮件中并没有拼写错误,并向我传授了关于网站开发的许多要点。

转眼 15 年过去了,如今我在各大会议上发表演讲,管理开源项目,合著图书,成了这个领域的“专家”。有时候我会问自己:“我是如何取得今天的成就的?”这个问题的答案绝不仅仅是任由时间一天天地流逝——至少不完全是虚度时光。

为什么需要同构 JavaScript

到目前为止,我的 Web 开发之路的最后一站是沃尔玛实验室的平台团队,我的同构 JavaScript 探索之旅就是从这里开始的。刚开始在沃尔玛工作时,我被分配到了一个负责开发 Web 新框架的团队。这个框架是从零开始开发的,其目标是支撑面向公众的大型网站。除了满足这类网站的最低要求,支持 SEO 并优化网页加载速度之外,保持 UI 工程师(包括我在内)的开发愉悦感和高效率也非常重要。很明显,为了更好地实现 UI 工程师的目标,我们的首选是基于现有的单页面应用(Single-Page Application,SPA,https://en.wikipedia.org/wiki/Single-page_application)技术进行扩展。但 SPA 模型有一个问题,它不能很好地支持我们的最低要求(详情请参见下文中的“完美风暴:一个极其平常的故事”和 1.2.3 节中的“单页面 Web 应用”),所以我们最终选择采用同构的方式。以下是这样做的理由。

  • 对于同一个渲染周期,客户端和服务器端可以使用同一套代码。这意味着不需要重复劳动,可以在降低界面开发与维护成本的同时提高团队开发速度。
  • 在服务器端渲染一份初始的 HTML 代码后,用户会感觉网页的加载速度更快,这是因为用户可以在浏览器中先看见首屏渲染的内容,而无须等待应用资源的加载和数据抓取完成。在网络延迟比较严重的环境中,这种改进页面预加载的方式尤为重要。
  • 同构应用支持 SEO,因为同构应用使用的 URL 不包含 # 号片段。此外,在那些不支持 History API 的浏览器中,它可以(对后续的每次请求)优雅地降级为服务器端渲染的方式。
  • 在支持 History API 的浏览器中,同构应用使用了 SPA 模型的分布式渲染,因此后续请求可以减轻服务器负载。
  • 无论是在服务器端还是在客户端,UI 工程师都可以完全掌控界面(https://www.nczonline.net/blog/2013/10/07/node-js-and-the-new-web-front-end/),而且同构应用在前后端之间划分了明确的界限,这有助于降低操作成本。

以上是我们团队走上同构 JavaScript 之路的主要原因。但在详细介绍同构 JavaScript 之前,先交代一些背景知识是很有必要的,这可以帮助你理解我们今天所处的具体环境。

平台的演进

Web 技术的快速演进令人难以想象。当初,CSS 和 JavaScript 技术被引入浏览器的目的是提供一种交互模型和关注点分离。你还记得曾有无数的文章提倡结构、样式和行为分离吗?即便在引入了这些技术之后,应用的架构也并没有发生太大变化。文档通过 URI 的形式进行请求,浏览器解析返回内容后,再进行渲染。唯一的不同之处就是 JavaScript 让界面变得更丰富了一点。直到微软公司引入一项被称为 XMLHttpRequesthttps://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest)的新技术,Web 才在这项技术的催化下演进为应用平台。

Ajax:应用平台的崛起

尽管很多前端工程师对微软公司及其 IE 浏览器嗤之以鼻,但他们仍然应当对微软怀有一份感激之情。如果没有微软,前端工程师未必能达到今天的职业高度。如果 XMLHttpRequest 技术没有出现,Ajax 技术也就不会诞生;如果没有 Ajax,就不会有这么多修改页面内容的需求,我们也就没有必要使用 jQuery(https://jquery.com/)。你猜接下来会发生什么呢?我们就不会有大量的前端 MV* 库可供选择,单页面应用的模式也不会出现,进而 History API 也不会出现。所以,下一次抱怨 IE 浏览器给你带来麻烦时,请你务必对微软作出客观评价,毕竟微软改变了历史进程,为今天的 Web 应用奠定了基础,还为你提供了一个锻炼思维的场所。

Ajax:技术债的积累

虽然 Ajax 技术影响了 Web 平台的发展进程,但它也以技术债的形式造成了一些破坏性的影响。Ajax 模糊了以前清晰定义的模型。过去,当用户导航到一个新页面或者提交表单数据时,浏览器只需依次发送请求、取得响应并解析完整的文档流。当 Ajax 成为 Web 开发的主流技术后,这种方式完全改变了。现在,工程师不需要向服务器请求重新获取整份文档,只需要根据用户的请求来判断是加载更多数据还是返回另一个视图。这意味着应用可以仅更新页面中的某个区域。这种特性大大优化了客户端和服务器端的性能,同时明显改善了用户体验。不幸的是,客户端的应用架构几乎变得不存在了,而本来负责处理视图层的那些工程师并没有应对这种范式转变的经验。日积月累,这些因素将应用的维护工作变成了噩梦。由于模型定义变得模糊,Web 开发从此经历了一段艰难成长的时期。

完美风暴:一个极其平常的故事

想象一下如下情景:你所在的团队负责维护一个商业应用的商品页面。在该页面的右侧有一个用于显示用户点评的轮播组件,用该组件可以翻页。当用户点击翻页按钮时,客户端会改变 URI 并向服务器发起请求以重新获取整个商品页面。这种低效的做法会让身为工程师的你感到非常苦恼。你认为没有必要刷新整个页面并调用一个重新渲染页面的数据请求,真正需要获取的只是下一页评论的 HTML 内容。好在你一直在跟踪行业内最新的技术发展,准备尝试使用最近学习的 Ajax 来解决这个问题。你收集了一些关于 Ajax 的概念证明,并向上司推荐了这项技术。这时候的你看起来就像巫师一样。像其他技术一样,这项技术最终会正式投入到生产环境中。

你对目前的成果感到非常满意,直到后来了解到一种新的数据交换格式。这种称为 JSON 的格式受到了 JavaScript 的非官方代言人 Douglas Crockford(http://www.crockford.com/)的极力推荐。你马上就觉得目前的实现不够完美了。第二天,你使用一种称为微模板(micro-templating,http://ejohn.org/blog/javascript-micro-templating/)的技术编写了一份新的概念证明,并再次向上司推荐。这项技术同样受到了好评并再次投入到生产环境中。

此时普通的工程师已经将你奉为神明。这时候,代码在审查阶段被发现有 bug。上司找到你并让你修复 bug,因为这段逻辑是你实现的。你审查了代码,并向上司宣称 bug 在服务器端的渲染中。然后你需要对使用两套渲染方案的原因进行一番解释。在解释完为什么 Java 不能在浏览器中运行后,你还得向上司保证这种实现是值得的,因为这样可以大大提升用户体验。这个问题像烫手的山芋那样被传来传去,直到最后才得以解决。

尽管违背了 DRY(don't repeat yourself,不要重复你自己)原则,但你依然被誉为专家。你的实现模式逐渐被大家学习、模仿,进而充斥着整个代码库。但随着这种模式的渗透,意料之外的事情发生了。bug 的数量开始不断上升,开发人员开始害怕因修改代码而造成的回归问题。目前欠下的技术债甚至比国家的财政赤字还要严重。工程经理和开发人员开始互相推卸责任。应用变得非常脆弱,公司也因此难以应对市场的快速变化。你感到一种强烈的罪恶感。幸好,你发现了一种叫作单页面应用的新模式……

客户端架构的救赎

近段时间,你阅读了一些文章,内容主要是人们对于前端架构的缺失而感到沮丧。这些人通常会将责任归咎于 jQuery,尽管这个库本来只是对 DOM 操作的封装(façade)。好在业界有人遇到了和你一样的困境,并且没有在其他不明真相的人的评论中停止自己的脚步。其中一个人就是 Backbone(http://backbonejs.org/)框架的作者 Jeremy Ashkenas(https://github.com/jashkenas)。

你开始了解 Backbone,阅读相关文章,并且深感兴趣。Backbone 将应用逻辑从数据检索中抽离出来,并将界面代码整合为单一语言和运行时,因此可以有效减少服务器端的压力。“找到了!”你在心中欢欣鼓舞地喊道。这个框架将解决我们遇到的所有问题。你又提出了一份新的概念证明,并开始实施。

在我们访问时发生了什么

你很快就被称为救世主。这种新的 SPA 模式在公司范围内被广泛接受。bug 的数量开始减少,工程师又重拾了信心。交付代码时的恐惧感几乎已经消失。这个时候,负责产品的同事找到你,并告知你自从实现 SPA 模式之后,网站的访问量下降了。你得想办法处理 # 号片段带来的问题了。经过一番详尽的研究后,你确定问题出在搜索引擎没有考虑 URI 中的 window.location.hash 部分,而 Backbone.Router 用这部分来创建可跳转、可收藏书签、可分享的页面视图。因此,当搜索引擎爬取这个应用时,没有任何可以收录的内容。现在你面临的形势更加严峻了,因为这个问题会对商品销量产生直接影响。因此,你再次开启了调研与开发的循环。结果你发现有两种方案可供选择:第一种方案是运转新的服务器,模拟 DOM 操作以运行客户端应用,并将搜索引擎重定向到这些新服务器上;第二种方案是付费让其他公司为你提供解决方案。除了 SPA 实现给公司带来的损失外,这两种方案都需要支付额外成本。

同构 JavaScript:一个美好的新世界

上述这个故事综合了我的个人经历以及我从其他工程师那里目睹或听说的故事。如果你也曾为某个 Web 应用付出很多时间,我相信你也会有类似的经历和感受。故事当中的某些问题已经成为了历史,但部分问题依然存在。此外还有一些问题没有明说,例如页面加载速度有待优化以及缺少感知渲染。如果将路由的响应与渲染的生命周期合并为一个通用的代码库,并同时支持在客户端和服务器端运行,应该就可以解决上述这些问题以及其他潜在问题。这就是同构 JavaScript 的意义所在。同构 JavaScript 应用是整合两种架构,以创建易于维护的、更好的用户体验。

未来的路

本书的主要目的是提供实现同构 JavaScript 所需的基础知识,帮助你理解业界现有的同构 JavaScript 解决方案。本书旨在提供足够多的信息,让你在实际中判断同构 JavaScript 是否为可行的解决方案,同时介绍业内最先进的解决方案,避免你重复造轮子。

第一部分是对这个主题的介绍。首先,详尽地介绍现有的几种 Web 应用架构,内容涵盖同构 JavaScript 的基本原理和用例,如 SEO 支持和提升页面的感知加载速度。然后,概述不同种类的同构 JavaScript 应用,如实时应用与类似 SPA 模式的应用。此外,还介绍了同构应用方案的组成部分,其中包括提供环境 shim 和抽象的实现,以及真正与环境无关的实现。该部分将为第二部分奠定代码基础。

第二部分将主题分解为关键概念,这些概念在大部分同构 JavaScript 解决方案中被普遍使用。每种概念都无须依赖现有的库[如 React(https://facebook.github.io/react/)、Backbone(https://facebook.github.io/react/)或 Ember(https://facebook.github.io/react/)]即可实现。这样做是为了避免将概念和某种特定的解决方案混淆。

第三部分将会介绍业内的专家是如何在他们的解决方案中作出权衡的。

排版约定

本书使用了下列排版约定。

  • 黑体

    表示新术语或重点内容。

  • 等宽字体(constant width

    表示程序片段,以及正文中出现的变量、函数名、数据库、数据类型、环境变量、语句和关键字等。

  • 加粗等宽字体(constant width bold

    表示应该由用户输入的命令或其他文本。

  • 等宽斜体(constant width italic

    表示应该由用户输入的值或根据上下文确定的值替换的文本。

代码示例

补充材料(代码示例、练习等)可以从 https://github.com/isomorphic-javascript-book 下载。

本书是要帮你完成工作的。一般来说,如果本书提供了示例代码,你可以把它用在你的程序或文档中。除非你使用了很大一部分代码,否则无须联系我们获得许可。比如,用本书的几个代码片段写一个程序就无须获得许可,销售或分发 O'Reilly 图书的示例光盘则需要获得许可;引用本书中的示例代码回答问题无须获得许可,将书中大量的代码放到你的产品文档中则需要获得许可。

我们很希望但并不强制要求你在引用本书内容时加上引用说明。引用说明一般包括书名、作者、出版社和 ISBN。比如:“Building Isomorphic JavaScript Apps by Jason Strimpel and Maxime Najim (O'Reilly). Copyright 2016 Jason Strimpel and Maxime Najim, 978-1-491-93293-3”。

如果你觉得自己对示例代码的用法超出了上述许可的范围,欢迎你通过 permissions@oreilly.com 与我们联系。

Safari® Books Online

{40%}

Safari Books Online(http://www.safaribooksonline.com)是应运而生的数字图书馆。它同时以图书和视频的形式出版世界顶级技术和商务作家的专业作品。技术专家、软件开发人员、Web 设计师、商务人士和创意专家等,在开展调研、解决问题、学习和认证培训时,都将 Safari Books Online 视作获取资料的首选渠道。

对于组织团体、政府机构和个人,Safari Books Online 提供各种产品组合和灵活的定价策略。用户可通过一个功能完备的数据库检索系统访问 O'Reilly Media、Prentice Hall Professional、Addison-Wesley Professional、Microsoft Press、Sams、Que、Peachpit Press、Focal Press、Cisco Press、John Wiley & Sons、Syngress、Morgan Kaufmann、IBM Redbooks、Packt、Adobe Press、FT Press、Apress、Manning、New Riders、McGraw-Hill、Jones & Bartlett、Course Technology 以及其他几十家出版社的上千种图书、培训视频和正式出版之前的书稿。要了解 Safari Books Online 的更多信息,我们网上见。

联系我们

请把对本书的评价和问题发给出版社。美国:

  O'Reilly Media, Inc.

  1005 Gravenstein Highway North

  Sebastopol, CA 95472

中国:

  北京市西城区西直门南大街 2 号成铭大厦 C 座 807 室(100035)

  奥莱利技术咨询(北京)有限公司

O'Reilly 的每一本书都有专属网页,你可以在那儿找到本书的相关信息,包括勘误表、示例代码以及其他信息。本书的网页地址是:

  http://shop.oreilly.com/product/0636920042846.do

对于本书的评论和技术性问题,请发送电子邮件到:

  bookquestions@oreilly.com

要了解更多 O'Reilly 图书、培训课程、会议和新闻的信息,请访问以下网站:

  http://www.oreilly.com

我们在 Facebook 的地址如下:

  http://facebook.com/oreilly

请关注我们的 Twitter 动态:

  http://twitter.com/oreillymedia

我们的 YouTube 视频地址如下:

  http://www.youtube.com/oreillymedia

致谢

Jason Strimpel

首先,我要感谢妻子 Lasca 的耐心与支持。我每天都被你的智慧、幽默、慈悲和爱所包围。感谢你选择了和我一起生活,这是我莫大的荣幸。是你让我成为了更加优秀的人,感谢你对我的爱。

其次,我想感谢与我合著本书的同事 Maxime。没有你的激情、知识、想法和专业知识,我的努力将大打折扣。你对于软件架构的洞察力每天都能给予我灵感。非常感谢你。

感谢我的编辑 Allyson。你提出的观点、疑问和修改意见让我的写作增色不少。感谢你。

最后,我要感谢第三部分中所有内容的贡献者,感谢你们在百忙之中抽空和读者分享你们的故事。这一部分展示了同构 JavaScript 的深度以及解决方案的多样性。你们用独特的解决方案证明了每一位工程师都是富有创造力的。感谢你们。

Maxime Najim

首先要感谢我的家人——我的妻子 Nicole,以及我的孩子 Tatiana 和 Alexandra,感谢他们在本书的编写和出版过程中给予我的支持与鼓励。

我也永远感激 Jason 邀请我和他合著本书,能与你合作让我感到非常荣幸。这是一个千载难逢的机会,是你的智慧、知识和努力把这个机会变成了现实。我对你感激不尽。同样,非常感谢我们的编辑 Allyson 在整个过程中提供了宝贵的支持和建议。

最后,我要特别感谢第三部分内容的贡献者在百忙中抽空与我们分享他们的故事和经历。非常感谢你们!

第1章 为什么需要同构 JavaScript

Jason Strimpel、Maxime Najim

2010 年,Twitter 对其网站进行了一次重构,并发布了新的版本。这个称为“#NewTwitter”的新版本将 UI 渲染和业务逻辑放在了 JavaScript 中,并在用户的浏览器中运行。这种架构在当时是开创性的。然而,不到两年的时间,Twitter 再次进行了重构,将渲染功能移回了服务器端。Twitter 的这次改版将页面的初始渲染时间缩短到了原来的五分之一(https://blog.twitter.com/2012/improving-performance-on-twittercom)。Twitter 的做法在 JavaScript 社区中引起了轰动。开发者和其他许多人很快意识到,客户端渲染对性能有着非常明显的影响。

构建客户端 Web 应用的最大劣势在于,首次加载需要付出高昂的代价下载一个 JavaScript 大文件。互联网中的主要传输协议是 TCP(Transmission Control Protocol,传输控制协议),该协议定义了一种被称为慢启动(slow start)的拥塞控制机制,这意味着数据是以逐渐增加数据块的方式进行发送的。Ilya Grigorik 在《Web 性能权威指南》{1[此书已由人民邮电出版社出版,http://www.ituring.com.cn/book/1194。——编者注]} 一书中解释了 TCP 协议如何经过“客户端与服务器端之间的 4 次往返……以及几百毫秒的延迟,才能达到 64KB 的吞吐量”。显然,发送给用户的前几千字节的数据对良好的用户体验和页面响应性至关重要。

客户端 JavaScript 应用在初始化时只包含一个 <script> 标签和一个空的 <body> 标签,这类应用的崛起产生了一些问题:初始化加载速度慢、需要对 URL 进行 hashbang(#!)的特殊处理(随后将对此进行详细介绍),以及糟糕的搜索引擎检索性。通过将客户端和服务器端代码合二为一,同构 JavaScript 解决了这些问题。同构 JavaScript 提供了整合两种架构的能力,可以创建易于维护的、用户体验良好的应用。

1.1 定义同构 JavaScript

简单来说,同构 JavaScript 应用就是在浏览器客户端和 Web 应用服务器端间共享同一套 JavaScript 代码的应用。从某种意义上讲,之所以称为同构,是因为无论在客户端还是在服务器端运行,应用都具有相同的形式或形态。同构 JavaScript 是 JavaScript 发展进程中的革命性一步。但就像钟摆一样,软件开发中的进步通常不稳定,来来回回。如果从事软件开发已有一段时间,那么你可能已经了解过一些时隐时现的设计方法。在某些情况下,我们似乎永远无法找到正确的平衡点。

近 20 年来,Web 应用的发展方式非常符合这一规律。我们见证了 Web 的演进——从最初简陋的蓝色超链接静态页到如今用户体验丰富、可以媲美成熟原生应用的平台。之所以能做到这一点,是因为 Web 的客户端 - 服务器模型迅速从重服务器端轻客户端的方式转变为轻服务器端重客户端的方式。然而,这种方式的转变导致了大量问题,我们将在本章后面具体讨论。就目前而言,可以简单地概括为我们需要在重客户端重服务器端之间取得平衡。为了真正了解这种平衡的意义,我们必须先后退一步,看看 Web 应用在过去几十年里是如何发展的。

1.2 评价其他的Web应用架构方案

要想理解同构 JavaScript 方案的由来,必须先了解这个方案出现时的状况。首先要确认主要的使用场景。

第 2 章中介绍了两种类型的同构 JavaScript 应用并分析了其架构。本书探讨的同构 JavaScript 应用场景是电子商务相关的 Web 应用。

1.2.1 状况的改变

万维网(World Wide Web)的出现要归功于 Tim Berners Lee(https://www.w3.org/People/Berners-Lee/)。当时他在一个核研究机构中工作,并在一个名为 Enquire(https://en.wikipedia.org/wiki/ENQUIRE)的项目中尝试使用了超链接技术。1989 年,Tim 整理了超链接的概念,提议在一个提供文档链接的中央数据库中应用这项技术。随着时间推移,数据库变得越发庞大,对我们的日常生活(如通过社交媒体)和商业(电子商务)产生了巨大影响。我们这些青少年都深陷到这个虚拟的大商场中。丰富多样的内容和购物选项能够帮助我们在购买时作出明智的决定。在意识到消费者的选择多如牛毛后,企业非常关心我们能否找到并查看他们的内容与商品,因为他们的最终目标是提高转换率(让我们购物)。为此,甚至还出现了专门负责搜索引擎优化(search engine optimization,SEO)的专家,这些专家唯一的工作就是让企业的内容和商品出现在搜索结果前列。然而,有关转换率的斗争并没有到此结束。一旦消费者找到商品,页面必须能够快速加载并响应用户的交互操作,否则企业可能会将消费者拱手让给竞争对手。这正是我们身为工程师应当发挥作用的地方,而除了企业的关注点外,我们还有自己的一系列关注点。

1.2.2 工程上的关注点

作为工程师,我们也有自己的一些担忧,主要关于可维护性和效率,但这并不是说我们在权衡技术决策时不会考虑企业的关注点。事实上,优秀工程师的做法恰恰相反:他们会基于手头的业务问题权衡短期和长期的利弊,为每个可能发生的业务问题寻找最优解。

1.2.3 可选架构

考虑到我们的主要业务场景是电商应用,我们来看看在 Web 发展史中适用于该场景的几种架构。但在此之前,我们先要明确一些关键的评判标准,以便公正地评估不同的架构。以下标准是按照重要性排序的。

(1) 应用要能够收录在搜索引擎中。

(2) 应用的首屏加载速度应该是优化过的,也就是说,关键渲染路径(critical rendering path)应该属于初始响应的一部分。

(3) 应用要能够响应用户的交互操作(比如优化后的网页切换)。

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

在整个评估过程中,必须权衡这些业务标准和工程的主要关注点(可维护性和效率)。

  1. 传统的 Web 应用

    前面提到过,设计和创造 Web 的最初目的是共享信息。由于万维网的提出是以 Enquire 项目的成功作为前提的,因此 Web 在起步阶段仅仅用于多页文档的相互连接不足为奇。20 世纪 90 年代初,大部分的 Web 内容都是以完整的 HTML 页面的形式渲染的。当时支持这种方式的机制是 HTML、URI 和 HTTP(现在也依然如此)。HTML(Hypertext Markup Language,超文本标记语言)是一种标记规范,当浏览器解析标记时,会将其转换为文档对象模型。URI(Uniform Resource Identifier,统一资源标识符)用于标识资源的名称,即应该响应请求的服务器的名称。HTTP(Hypertext Transfer Protocol,超文本传输协议)是负责连接一切的传输协议。这三种机制为互联网提供了动力,并形成了传统 Web 应用的架构。

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

    {%}

    图 1-1:传统 Web 应用的流程

    简而言之,上图展示了传统 Web 应用的架构。我们来研究一下这个架构是否符合我们的评估标准和工程上的关注点。

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

    我们所说的“感知的渲染速度”是什么意思呢? Ilya Grigorik 在《Web 性能权威指南》一书中是这样解释的:“时间测量是客观的,而时间感知是主观的。我们可以通过设计来改善感知性能。”

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

    Ajax 时代。星星之火可以燎原,而 XMLHttpRequesthttps://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest)正是点亮 Web 平台的星火。然而,这项技术在集成到传统 Web 应用中时并没有给人留下太深刻的印象,这并不是因为设计或者技术本身的原因,而是因为负责集成该技术到传统 Web 应用中的那些人缺乏使用经验。在大多数情况下,负责该工作的人都是刚开始专攻视图层的设计师。我自己是从行政助理转为设计师和开发者的。当时,我的这两项技能都不足。不用说,我对过去参与过的应用造成了很大的破坏(不过我认为这是我对平台演进的贡献)。不幸的是,在这段演进期,我和那些缺乏适当培训与指导的人们接触的所有应用都受尽了苦头——它们的过程重复、关注点混乱。有一个很好的例子可以突出这些问题,即相关商品的轮播组件(如图 1-2 所示)。

    {%}

    图 1-2:商品轮播组件示例

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

    {%}

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

    由于启用了 Ajax,再加上看似美好的初衷,导致了 UI/ 视图层的分裂与复制,一个看似精心构造的应用就此化作瓦砾,从而让无数工程师遭受了挫折。好在工程师在沮丧的时候通常是最有创造力的。正是这种挫折推动了创新,再结合工程师扎实的工程技能,便造就了下一代应用架构。

  2. 单页面 Web 应用

    一切事物都有自己的循环周期。在 Web 开始阶段时流行的轻客户端可能给了 Sun Microsystems 的 NetWork Terminal(NeWT,https://en.wikipedia.org/wiki/Sun_Ray)以启发。但到了 2011 年,Web 应用开始放弃轻客户端模型,并过渡到重客户端模型,而操作系统领域在多年以前就发生过这样的变化。巨石已经浮出水面。这就是单页面应用架构的黎明。

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

    {%}

    图 1-4:单页面应用的流程

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

    现在回到之前讨论的产品轮播组件示例,第一页(相关)产品的信息在以前是由应用服务器渲染的。在翻页时,客户端负责发起随后的请求并进行渲染。在现代 Web 平台中,这种职责的模糊界限和工作的重叠正是传统 Web 应用面临的主要问题。但这些问题在 SPA 中将不复存在。

    在 SPA 中,服务器端和客户端之间存在明确的界限。API 服务器响应数据请求,应用服务器提供静态资源,而客户端则负责展示。在这个产品轮播的例子中,应用服务器会向浏览器发送一份仅包含 JavaScript 静荷数据和模板资源的空文档。客户端应用在浏览器中进行初始化并向服务器请求渲染视图所需要的数据,视图中包含了轮播组件。收到数据后,客户端应用将会为这次轮播渲染产品的第一组集合。在翻页时,请求数据和渲染的生命周期会再次重复,并且复用同一段代码路径。这确实是一种优秀的工程解决方案,但问题是,这种方案并不能在任何时候都提供最佳的用户体验。

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

    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 是同一个页面。好在谷歌定义了一种变通方案,为 hash 片段提供了 SEO 支持,这个方案就是使用“#!”(hashbang)。

    大多数的 SPA 库目前已经支持 History API(https://developer.mozilla.org/en-US/docs/Web/API/History),并且谷歌的爬虫最近对于索引 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 对应的屏幕快照提供给爬虫。图 1-5 展示了该过程。

    {%}

    图 1-5:爬虫收录 SPA URI 的过程

    此时,SPA 的价值主张愈发下降了。从工程角度来说,需要在下列方案中二选一。

    (1) 在服务器中运行一个无界面的浏览器,如 PhantomJS(http://phantomjs.org/),用于在服务器中运行 SPA 并响应爬虫请求。

    (2) 将这个问题外包给第三方供应商解决,如 BromBone(http://www.brombone.com/)。

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

  3. 同构 JavaScript 应用

    同构 JavaScript 应用是传统 Web 应用和 SPA 架构的完美结合。同构应用具备以下优势。

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

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

1.3 附加说明:何时不使用同构

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

此外,很多公司和组织可能还没准备在服务器上操作和维护一个 JavaScript 的执行引擎。例如,大量使用 Java、Ruby、Python、PHP 的组织可能并不知道如何在生产环境中对一个 JavaScript 应用服务器(如 Node.js)进行监控与故障诊断。在这些情况下,同构 JavaScript 可能会引起难以承受的额外操作成本。

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

如果你的项目或者公司不需要借助同构 JavaScript 架构提供的便利(如本章所述),请务必针对具体工作选择合适的技术。然而,当服务器端渲染不在你的选择范围之内,而你又需要关注首屏加载速度和搜索引擎优化时,别担心,本书可以帮到你。

1.4 小结

我们在本章中定义了同构 JavaScript 应用——在浏览器客户端以及 Web 应用服务器端共享同一套 JavaScript 代码的应用——并确定了本书中主要讨论的同构 JavaScript 应用类型是电商应用。随后,我们回顾了 Web 的发展历史并研究了其他架构的发展历程,通过 SEO 支持、首屏加载速度优化和页面过渡效果优化这三个关键的验收标准评估了这些架构。我们看到,同构 JavaScript 出现之前的架构不能满足所有的验收标准。在本章最后,我们将传统 Web 应用和 SPA 的优势结合起来,得到了同构 JavaScript 应用架构。

第一部分 简介与关键概念
第2章 同构 JavaScript 图谱
第3章 同构 JavaScript 分类
第4章 超越服务器端的渲染
第二部分 构建第一个应用
第5章 起步
第6章 提供第一份 HTML 文档
第7章 设计应用架构
第8章 将应用传输到客户端
第9章 创建常用的抽象
第10章 序列化、反序列化和添加事件监听
第11章 结束感言
第三部分 现实世界的解决方案
第12章 沃尔玛实验室的同构 React.js 方案
第13章 全栈 Angular
第14章 Brisket
第15章 Colony 案例研究:脱离 Node 创建同构应用
第16章 结语
关于封面

阅读全文: http://gitbook.cn/gitchat/geekbook/5b5810dff5826561d244ed73

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页