富互联网应用与 AJAX
早期的交互式 Web 应用主要是基于表单的。用户会将数据输入 HTML 表单,然后由浏览器传输回 Web 服务器,在服务端处理数据并更新数据库,最后将更新的 HTML 文稿传输回浏览器显示。JavaScript 在浏览器端用于基本的输入数据验证,以及对服务端生成的 HTML 做简单的动态更改。这种 Web 应用的形式后来被表述为 Web 1.045。
一些应用程序具有高度的交互性,需要丰富的低延迟用户界面。于是不可避免地,有些开发者想要开发具备这些特性的 Web 应用。当 Netscape 在 1995 年将 Java 和 JavaScript 引入 Web 浏览器时,其计划是 Java 将成为实现复杂交互式 Web 应用的主要语言,而 JavaScript 将主要用于基于表单的应用中 [Shah 1996]。在 1990 年代末和 2000 年代初,许多「富互联网应用」[Allaire 2002] 被构建为 Java Applets。
在 1997 年,微软发布了其企业电子邮件客户端的 Web 版本,这就是被实现为 Web 1.0 风格的 Outlook Web Access(OWA)[Bilic 2007, Van Eaton 2005] 应用。而后,OWA 1.0 被交互更丰富的版本所接替。这个新版本使用了动态 HTML(Dynamic HTML46)和一个名为 XMLHTTP [Hopmann 2006] 的新浏览器 API。XMLHTTP 使得网页上的 JavaScript 代码能与服务端来回异步传输数据,而无需完全重新加载网页。DHTML 和 XMLHTTP 的组合,使得 Web 页面在每个会话中只需加载一次,然后即可作为支持远程访问数据和服务的交互式应用而运行。
在 2000 年代上半叶,许多组织都使用过类似的技术来构建 Web 应用。但直到 Google 用它来实现 GMail、Google Maps 和其他应用后,这种 Web 应用风格才广为人知。Jesse James Garrett [2005] 创造了「AJAX」一词来形容它。AJAX 和使用它构建的社交媒体应用,成为了 Web 2.0g 时代的标志。
Web 2.0 和 AJAX 的出现,是 JavaScript 在 Web 开发中用途的主要转折点。JavaScript 的角色逐渐由一门用来将动态元素添加到静态页面的语言,变为了一门用来对复杂的富互联网应用(RIA)进行编码的语言。
同时,浏览器的生态系统正变得越来越复杂,总有各式各样市场份额很低的新浏览器出现。Netscape(在被 AOL 收购后)和微软(在获得市场主导地位后)逐渐放弃了对浏览器的活跃开发,这为新浏览器的出现创造了机会。Firefoxg47 [Mozilla 2004]、Operag [Opera 2013]、苹果 Safarig [Melton 2003],以及最后的谷歌 Chromeg [Kennedy 2008] 逐渐占据了有意义的市场份额。
新的浏览器都实现了对 JavaScript ES3 规范的支持,也支持被 W3C 部分指定的浏览器平台 API。但由于平台规范并不够完整和精确,大多数新浏览器都以各种方式扩展或修改了平台的 API。并且尽管这些新浏览器不断涌现,许多用户仍在使用过时的 Internet Explorer 和 Netscape 版本。这些版本有很多 bug,并缺乏对最新语言特性和平台 API 的支持。
在一个重要的维度上,Web 浏览器与大多数其他应用平台有所不同,那就是应用程序以源码形式分发,以便在用户提供的环境中立即执行。这与传统的方案是不同的。在传统方案中,开发者可以选择特定版本的编译器和运行时库,然后在以二进制形式将其部署给用户前,构建和测试其应用。Douglas Crockford48 在一些演讲中,将 Web 开发的这一特色描述为:由用户(通常不知情地)选择语言的处理器。Web 开发者需要确保其 Web 页面和 Web 应用能在最终用户选择的任何浏览器上正常工作。
解决浏览器差异的一种方法,是为每个不兼容的浏览器创建单独的应用版本。然后 Web 服务器就可以在收到网页请求时,根据浏览器提供的标识信息,将不同版本发送到不同的浏览器。但是大多数应用的源码通常都由其所有版本共享,只有很小的变化会用来解决浏览器的差异。这就产生了维护应用程序多个(基本相同的)版本时的开发和运营挑战。
一种避免应用源码出现多个不同版本的方法,是维护单个源文件。当应用在浏览器中运行时,它会动态选择出特定于浏览器的变体版本。这里对变体的选择方式,主要是基于惯用的代码序列,包括执行浏览器嗅探(识别出特定的浏览器版本)或功能测试(识别出某种特性或 bug 是否存在)。
在 AJAX 应用复杂性与浏览器兼容问题的背景下,用于简化 Web 应用构建的框架和库应运而生。早期的框架包括 Prototype [Stephenson et al. 2007]、MooTools [Proietti 2006] 和 Dojo [Russell et al. 2005],而其中最受欢迎 [W3Techs 2010] 的是 jQuery [Resig 2006]。这些早期的框架与库通常为 AJAX 应用提供了基础结构支撑,并为简化编码实现常见任务提供了高层面的抽象。它们还通过内部处理和隐藏许多浏览器特性变体的方式,解决了许多兼容问题。
这样一种特殊的库,已经重要到了要创造新词汇来表示它的程度。Remy Sharp [2010] 提出了「polyfillg」一词,它所描述的库提供了「应由浏览器提供但仍然缺失」的 API 支持。设计良好的 polyfill 会动态检查它所提供的特性是否在环境中已经可用。只有在缺少内置支持或不兼容的情况下,polyfill 才会自行将其置入环境。早期的 polyfill 库专注于使浏览器更具互操作性,其手段主要是隐藏早期浏览器竞争中留下的遗留特性变体,或在旧浏览器中支持新的浏览器特性。如果一个特性在某种流行的浏览器中存在,但在其他流行的浏览器中却不存在,那么 polyfill 可以使 Web 应用使用相同的代码在所有浏览器上运行。随着浏览器兼容性的改善,polyfill 则成为了一种常见手法,用来尽早用上浏览器和 JavaScript 的新特性。在 Web 新特性的设计过程中,polyfill 库的创建变得十分普遍。除了对开发者有用外,通过 polyfill 还能收集到宝贵的开发者反馈,从而支持新特性和 API 的设计。
当 JavaScript 应用是朴素地将独立创建的几个部分组合而成时,命名冲突十分常见。许多框架和库提供了某种模块化机制,这通常是通过使用命名空间对象(namespace objects)和立即执行的函数表达式(IIFE49)来实现的。命名空间对象只是个单例对象,其主要用途是提供对函数或变量的限定(qualified)名称访问。JavaScript 1.0 的内置 Math 对象就是命名空间对象。命名空间对象的限制之一在于,它之中的所有名称都是公共的。要克服这个限制,可以将命名空间对象与 IIFE 相结合,如图 22 所示。
// 使用模块模式定义 services
var Services = function () {
var privateJobCount = 0; // 「模块」的私有状态
return { // 命名空间对象
jobCount: function () { return privateJobCount },
job1: function () { privateJobCount++ }
}
}(); // Services 被初始化为调用该函数时的返回值
// 从命名空间里获取实体
Services.job1();
图 22. JavaScript 模块模式的示例。这里的 Services
函数封装了私有的实现。Services
会在被调用并返回命名空间对象时初始化,命名空间对象的属性暴露了「模块」的公共接口。
模块模式有几个变体,但基本概念都是用 IIFE(或有时用命名函数)的词法作用域,来封装一系列函数的某些私有状态。IIFE 会返回一个命名空间对象,其属性就是封装后需要支持被公开访问的函数。
通常认为 Douglas Crockford 普及了模块模式,但它很可能是由许多 JavaScript 程序员独立发现的。