在本文中,我们将着重讨论为何Web 的进化和前端开发的变革会促使Angular2诞生。
web 的进化-新框架时代
近年来,web 已经发生了大幅度的进化。在实现ECMAScript 5 的同时,ECMAScript 6也开启了它的发展历程(即现在众所周知的ECMAScript 2015,也叫ES2015)。ES2015对这门语言做了大幅度修改,例如:对模块化、块级作用域增加了语言级的内置支持;同时增加了很多语法糖,例如:支持class 以及解构赋值。
与此同时,Web Component 的概念被发明出来。Web Component 允许我们自定义HTML 标签并在上面绑定行为。在现有的HTML 标签基础上扩展新标签(例如对话框、图表、数据表格等)是很难的,主要原因是把这些新标签的API 进行巩固和标准化需要很长时间。更好的解决方案是允许开发者按照自己的想法去扩展现有的标签。WebComponent 带来了很多优势,例如更好的封装性、我们提供的标签具有更好的语义、更好的模块化,并且能让开发人员和设计师之间的交流更加顺畅。
我们知道JavaScript 是一门单线程语言。最初开发这门语言的时候,目标只是用来编写简单的客户端脚本,但是随着时间的推移,它的角色发生了很大的转变。现在,我们可以利用HTML5 提供的API 来处理音频和视频文件,用全双工通道与外部服务进行通讯,传输和处理大块原始数据,如此等等。如果所有这些耗时运算都在主线程里面执行的话,用户体验会非常糟糕。因为在执行耗时运算的时候,用户界面会处于冻结状态。正是这一点导致了WebWork 技术的出现,WebWork 允许在后台执行脚本,然后与主线程之间通过消息机制进行通讯。这样一来,多线程编程就被引入到了浏览器中。
以上这些API,有一些是在AngularJS 1.x 开始开发之后才发明出来的,这就是为什么在AngularJS 1.x 中并没有用到它们中的大部分内容的原因。但是,把这些API 暴露给开发者可以带来很多好处,例如:
- 性能显著改善。
- 开发出来的软件质量更好。
现在,我们来简要讨论一下:如何在全新的Angular 内核中融合上面提到的这些技术?为什么要这样做?
ECMAScript 的进化
现在,浏览器厂商们都在以非常短的迭代周期不断发布新特性,用户会经常收到升级通知。这种状况也让开发者能够使用最前沿的技术改善web 现状从而推动web 的进化。ES2015 已经标准化,很多主流浏览器已经开始支持最新版的语言标准。作为开发者,学习和使用新语法不仅可以提升开发效率,而且也可以为不久的将来所有浏览器都支持新语法的那一天做好准备。所以,从现在就开始学习使用新语法变得很有必要。
某些项目可能会强制我们支持旧浏览器,而这些浏览器不支持任何ES2015 新特性。在这种情况下,我们可以直接编写ECMAScript 5(ECMAScript 5 标准发布于2009 年——译者注),它与ES2015 的语法虽然不同,但是语义上却是等价的。或者,我们可以利用预编译程序进行转码。我们可以利用ES2015 新语法来编写代码,然后利用预编译程序编译成浏览器所支持的目标版本。
AngularJS 发布于2009 年。当时大部分网站的前端代码都只支持ECMAScript 3,这是ECMAScript 5 发布之前的最后一个版本。这就意味着,那时候框架的实现语言当然就是ECMAScript 3。现在,如果要使用最新版的语言,就需要将整个AngularJS 1.x 全部迁移到ES2015 上去。
从一开始,Angular 2 就已经把web 的现状考虑在内,所以这个版本的框架使用了最新的语法。Angular 2 是用ES2016 的超集编写的(也就是TypeScript,稍后我们就来学习它),但是Angular 2 也允许开发者使用自己喜欢的语言去写代码。如果不喜欢对代码做预编译处理并且想简化构建过程,可以直接使用ES2015,甚至使用ECMAScript 5。
Web Component
Web Component 草案首次公开发表于2012 年5 月22 日,也就是AngularJS 1.x 发布的三年之后。如前所述,Web Component 标准允许我们创建自定义标签并增加行为。这一点听起来似曾相识,因为在AngularJS 1.x 应用中,我们已经在使用类似的概念开发用户界面了。
Web Component 听起来就像是Angular 指令的替代品,但是它的API 更加直观、功能更加丰富,而且有浏览器的内置支持。它还带来了很多其他优点,例如更好的封装。这一点非常重要,因为良好的封装可以有效地处理CSS 样式冲突问题。
如果要在AngularJS 1.x 中增加对Web Component 的支持,一种可行的策略就是:修改原有的指令实现,并在DOM 编译器中引入新的原语。作为Angular 开发者,我们都知道指令API 有多么强大而复杂。它涉及非常多的内容,如postLink、preLink、compile、restrict、scope、controller 等等,当然,还有我们最爱的transclude。
Web Component 标准被接受之后,未来众多的浏览器都将会为它提供底层实现上的支持,从而带来很多好处,例如:性能更高、更好的native API 支持。
在实现Web Component 的过程中,众多web 技术专家遭遇了Angular 团队在开发指令API 的时候所遇到过的相同难题,而最终解决方案却英雄所见略同。WebComponent 的背后有着众多设计方面的优秀决策,其中就包括content 标签,它可以用来解决AngularJS 1.x 里面声名狼藉的transclusion(嵌入)问题(transclusion 机制的作用是:把HTML 片段嵌入到模板里面,或者把模板嵌入到普通的HTML 标签里面去。transclusion 机制的用法极其繁琐而且难以理解,声名狼藉一词形容得还是比较中肯的——译者注)。
既然指令API 和Web Component 解决的是同样的问题,只是解决方法有所不同,那么在Web Component 的之上再保留指令API 就显得多此一举,而且增加了不必要的复杂性。这就是为什么Angular 核心团队从一开始就决定在Web Component 的基础上构建并全面支持新标准的原因。Web Component 标准引入了一系列新特性,其中很多特性某些浏览器还没有实现。如果我们的应用跑在浏览器里面,而浏览器却没有为某些新特性提供本地支持,那么Angular 2 将会模拟这些特性。其中一个案例就是ng-content 指令,它是content 标签的填缝剂(polyfill)(如果浏览器不支持某个新特性,polyfill 的作用是模拟这种新特性从而抹平这种裂缝,现在有很多这种小工具——译者注)。
WebWorker
JavaScript 以事件循环著称。一般来说,JavaScript 程序运行在单个线程里面,事件调度的策略是:把各种事件都push 到一个队列里面,然后再按照事件到达的顺序依次处理。然而,当其中某个预定的事件需要占用大量运算时间的时候,这种调度策略就显得不那么高效了。处理这种事件将导致主线程阻塞,并且所有其他事件都得不到处理,直到这个耗时的运算结束为止才能跳到队列中的下一个事件继续处理。针对这种情况举一个简单的例子:点击鼠标触发一个事件,在事件的回调函数里面使用HTML5 的音频API 来做一些音频处理。如果处理的音轨非常大,而且算法所需要的计算量很多,那么整个UI 就会冻结直到运算结束,这显然会影响到用户体验。
引入WebWorker API 就是为了填这些坑。WebWorker 允许在另一个线程里面执行计算密集型任务,从而解放主线程,让它可以处理用户输入并渲染用户界面。
那么,在Angular 里面如何使用WebWorker 呢?在回答这个问题之前,我们先来回顾一下AngularJS 1.x 里面的一些工作原理。假设有一个企业级应用,用来处理海量数据,这些数据都要通过数据绑定机制渲染到屏幕上,我们应该怎么做?每绑定一块数据都会添加一个新的监视器(watcher)。一旦digest 循环开始运行,它就需要遍历所有监视器,执行与之相关的表达,并把返回的结果与上一次遍历所获得结果做比较。这里有很多拖慢性能的地方:
- 遍历大量的监视器(watcher)。
- 在指定的上下文中执行表达式。
- 拷贝返回值。
- 把当前表达式的运算结果与上一次相比较。
以上所有步骤都有可能运行得非常慢,这和输入的数据量有关。如果digest 循环涉及密集的运算,为什么不把它移到WebWorker 中去?为什么不在WebWorker 内部执行digest循环,获取到发生变化的数据绑定,然后再把它们应用到DOM 上去呢?
为了达到这一目的,社区做了很多实验。然而,把这一机制融入到框架中并不容易。
效果不尽如人意的一个主要原因是,框架和DOM 操作紧紧耦合在一起。在监视器回调函数内部,Angular 经常直接操作DOM,从而无法把监视器移到WebWorker 中去,因为WebWorker 是在独立的上下文中被调用的,无法直接访问DOM。同时,在AngularJS 1.x中,各个监视器之间存在各种隐式或者显式的依赖关系,这就要求digest 循环执行多次才能获得稳定的结果。综合以上两点,结论就是:在主线程之外的独立线程里面监测改动很难获得成效。
如果在AngularJS 1.x 中处理这些问题,内部实现会变得相当复杂。因为框架一开始压根就不是基于这一机制构建的。而Angular 2 在启动设计之前WebWorker 已经获得了标准化,所以核心团队从一开始就已经把它考虑在内了。
从AngularJS 1.x 中学到的经验
为了顺应潮流,框架不得不进行重新实现,在上文里面介绍了关于这一点的一些争论,但是有一点我们必须牢记:我们现在并非白手起家,我们拥有从AngularJS1.x 那里所学到的经验。自从2009 年以来,并非只有web 在进化,我们也在开始构建越来越复杂的应用。今天,单页应用不再是标新立异之举,而更像是所有业务型web应用的标配。单页应用的定位是高性能和良好的用户体验。
利用AngularJS 1.x,我们已经可以构建高效、大规模的单页应用。然而,在大量的案例中使用之后,我们也发现了它的一些缺陷。为了满足这些新的需求,Angular 核心团队从社区中吸取了大量经验,开始运用全新的思路来进行开发。在看到Angular 2提供的新特性的同时,我们应该看到它是根据AngularJS 1.x 的经验发展而来的,然后再想一想,作为Angular 开发者,在过去的几年里面,那些困扰我们以及最终被解决掉的问题。