小编说:Angular 2 的最终版正式发布,Angular 1 的全平台继任者从此诞生。在上一篇文章中我们讨论了Web 的进化和前端开发的变革对Angular 2诞生的推动,不过不只如此, 1.x中存在的一些缺陷,不值得让我们继续在其中整合强大的工具。
本文选自《迈向Angular 2》,让我们看看Angular2解决了哪些在1.x版本中困扰我们的问题。
Controller
AngularJS 1.x 遵从了Model View Controller (MVC)的微架构模式。有人会争论说,它看起来更像 Model View ViewModel (MVVM),因为controller 有自己独立的语法,而视图数据模型是作为scope 或者当前上下文的属性而存在的。但是,如果我们使用Model View Presenter 模式(MVP)也能实现同样的目的。由于可以用各种不同的形式来组织应用逻辑,所以核心团队把AngularJS 1.x 称为Model View Whatever (MVW)框架。
在任何AngularJS 应用程序中,视图(View)都应该是由指令组而成的。各种指令互相协作,从而实现功能完整的用户界面。服务(Service)负责封装应用的业务逻辑。在服务代码中,我们可以通过HTTP 与 RESTful 服务进行通讯,使用WebSocket 甚至使用WebRTC 进行实时通讯。对于我们的应用来说,服务是实现领域模型和业务规则的基础构件。还有另外一个组件就是控制器(Controller),它主要负责处理用户输入并把执行过程代理给对应的服务。
虽然服务和指令都有明确的角色定义,但是在iOS 应用中,我们常常会看到MassiveView Controller 这种反模式。有时候,开发者会尝试在控制器中访问甚至直接修改DOM。一开始的时候,这种方式用来实现一些很简单的功能,例如修改标签的大小,或者快速粗暴地修改标签的样式。另一个值得注意的反模式就是:在不同的控制器中重复实现相同的业务逻辑。开发者倾向于拷贝粘贴这些逻辑,而实际上这些东西应该封装到service 里面去。
构建AngularJS 应用的最佳实践是:控制器根本不应该操作DOM,而是应该把访问和
操作DOM 的逻辑分离到指令中去。如果控制器之间有一些重复的逻辑,最大的可能就是:我们需要把这些逻辑封装到某个服务里面,如果某个控制器需要用到这些功能,就使用AngularJS 的依赖注入机制注入这个服务。
以上就是我们从AngularJS 1.x 中所学习到的内容。这样看来,似乎控制器的功能应该移到指令内部的控制器中去。由于指令支持依赖注入API,所以在接收到用户的输入之后,可以直接把具体的操作代理给注入的服务来执行。基于这一原因,Angular 2 中采用了完全不同的实现方案,删除了ng-controller 指令,解决了滥用该指令导致控制器满天飞的情况。
在《迈向Angular2》第4 章,将会学习如何用Angular 2中的组件和指令来取代AngularJS1.x 中控制器的功能。
Scope
AngularJS 中的数据绑定机制是利用scope 对象来实现的。我们首先在scope 对象上添加各种属性,然后在模板中显式声明需要绑定这些属性(单向绑定或者双向绑定都可以)。这种方案看起来很清晰,但是scope 还有两个更重要的职责:派发事件和实现基于脏值检测的行为。Angular 初学者需要花费大量精力去理解什么是scope 以及怎么使用scope。所以,AngularJS 1.2 引入了一个叫controller as syntax 的概念。它允许我们直接在控制器内部为当前上下文(this)添加属性,而不需要显式注入scope 对象然后再在上面添加属性。以下代码片段示范了这种简化的语法:
Angular 2 更进一步,直接删除了scope 对象。所有表达式都在特定UI 组件的上下文
中执行。把scope API 整体删掉之后使得Angular 2 得到了大幅度简化,我们不再需要显式注入scope 了,只要把属性直接添加到UI 组件上,然后再进行绑定操作即可。这种API 让人感觉更简单也更自然。
在《迈向Angular2》一书第4 章会详细学习组件和脏值检测机制。
依赖注入
在JavaScript 领域,AngularJS 1.x 也许是市面上的第一个通过dependencyinjection (DI)引入inversion of control (IoC)机制的框架。DI 可以带来很多好处,比如:易测试性、更好的代码结构和模块化,以及更简洁明了。虽然在1.x 版本中DI 运行得相当不错,但是Angular 2 对它进行了进一步的发挥。因为 Angular 2 是基于最新web 标准构建的,所以它使用了ECMAScript 2016 装饰器(decorator)语法对使用DI的代码进行了注解。这里的装饰器与Python 中的装饰器或Java 中的注解非常类似。它们都可以使用反射机制来decorate(装饰)指定对象的行为。由于装饰器还没有标准化,也不被主流浏览器所支持,所以使用的时候需要经过中间转换步骤。如果你不想这么麻烦,也可以直接用ECMAScript 5 语法编写一些冗长的代码去实现相同的语义。
新版本的DI 更灵活、功能更丰富,也消除了AngularJS 1.x 中的一些误区,例如API 不统一的问题。在 1.x 中,有些对象是根据参数的位置顺序注入的(例如scope、标签、属性,以及指令link 函数中的控制器);而其他对象则是根据名称注入的(例如在控制器,指令,服务和过滤器中会根据参数名称进行注入)。
在《迈向Angular2》一书第5 章会进一步学习依赖注入API。
服务端渲染
Web 需求越大,web 应用就变得越复杂。构建一个真实的单页应用需要编写大量的JavaScript 代码,把用到的所有外部类库全部一次性包含进来会导致页面上脚本的体积增加到好几兆。在移动设备上初始化应用可能要用几秒到十几秒的时间:从服务端获取所有资源、解析并执行JavaScript、渲染页面、应用所有样式。如果在低端移动设备上使用无线网络,这个过程可能会让用户放弃访问应用。虽然可以用一些技巧来加速这个过程,但是在复杂的应用中,没有银弹。
在尝试提升用户体验的过程中,开发者们发现了所谓的server-side rendering(服务端渲染)技术。它可以把单页应用中所请求的某个视图在服务端渲染好,然后把对应的HTML 直接发送给用户。随后,在所有资源处理完毕之后,脚本就会添加事件监听器并进行数据绑定操作。这样做看起来像是一个提升应用性能的好方法。使用此方法的先驱之一是ReactJS,它利用了Node.js 的DOM 实现在服务端预先渲染用户界面。可惜的是,AngularJS 1.x 的构架不支持这种特性。原因是框架和浏览器API 紧密耦合在一起,在WebWorker 中进行脏值检测的时候我们也遇到过同样的问题。
服务端渲染的另一个典型使用场景就是:构建对Search Engine Optimization(SEO,搜索引擎优化)友好的应用。为了让AngularJS 1.x 应用能够被搜索引擎索引,目前已经出现了很多hack 方法。例如,其中一种实战案例是这么处理的:使用无前端浏览器漫游整个应用,执行每个页面上的脚本并把渲染结果缓存成HTML 文件,从而让搜索引擎能够访问应用。
虽然这种变通方案可以构建对SEO 友好的应用,但是采用服务端渲染技术可以同时解决之前提到的两个问题:一是提升用户体验;二是用更简单优雅的方式来构建对SEO 友好的应用。
只要把Angular 2 和DOM 进行解耦,我们的应用就可以在浏览器之外的环境中运行了。为了实现这一目的,社区已经开发了一款工具,首先在服务端预先渲染单页应用中的视图,然后再转发给浏览器。本书在编写这段内容的时候,这款工具仍然处在开发的早期阶段,所以它并没有被包含在框架的内核中。
在《迈向Angular2》第8 章,我们将会深入学习这款工具。
大规模应用
自从Backbone.js 出现之后 ,MVW 就是构建单页应用的标配。我们可以按照注意点分离原则把业务逻辑从视图中分离出来,从而构建出设计良好的应用。MVM 可以使用观察者模式监听数据模型的改变,当发生改变的时候刷新视图。但是,其中的事件处理器之间存在一些显式或者隐式的依赖,这就使得应用中的数据流不清晰且难以理解。在AngularJS 1.x 中,不同的监视器之间可以相互依赖,从而导致了digest 循环必须进行若干次遍历,这些表达式的结果才能最终趋于稳定。所以,Angular 2 采用了单向数据流设计,优点如下:
- 更明确的数据流。
- 不同的数据绑定之间没有依赖关系,所以digest 没有存活时间(TTL)的概念。
- 性能更高:① digest 循环只运行一次。②创建对immutable/observable (不可变/可观察)数据模型友好的应用程序,从而可以做深度优化。
数据流的改变为AugularJS 1.x 基础构架带来了又一项根本性的变革。
当需要维护一个用JavaScript 编写的庞大的代码库时,我们可能要换一个角度来看数据流的问题。虽然JavaScript 的鸭子类型(指js 对象的动态特性——译者注)让这门语言非常灵活,但是同时也让IDE 和文本编辑器很难对代码进行分析和支持。对大型项目进行代码重构变得很难而且容易出错,原因是在大多数情况下进行静态分析和类型推断是不可能的。同时,在缺少编译器的情况下,很容易出现错别字,在跑测试用例或者真正运行应用之前很难发现这些错误。
Angular 核心团队决定使用TypeScript ,因为它有更好的工具,还有编译时类型检查;使用TypeScript 有助于提升生产效率,还能减少出错。如上图所述,TypeScript是ECMAScript 的超集,它引入了显式类型注解和编译器。TypeScript 代码会被编译成当前浏览器所支持的普通的JavaScript。TypeScript 从1.6 版开始,已经实现了ECMAScript 2016 装饰器,它是Angular 2 的完美选择。
各种IDE 和文本编辑器都可以更好地对TypeScript 进行静态代码分析和类型检查。所有这些优点都可以减少出错的概率,从而极大地提升生产率,同时还可以简化代码重构过程。TypeScript 另一个重要的隐含优点是使用静态类型带来的性能提升,因为JavaScript 虚拟机可以对静态类型进行运行时优化。
在在《迈向Angular2》第3 章中我们将详细讨论TypeScript。
模板
模板是AngularJS 1.x 的核心特性之一。模板是简单的HTML 并且不需要中间的处理和编译过程,这一点与mustache 之类的大多数模板引擎不同。AngularJS 中的模板简洁而强大,我们可以在模板内部创建Domain Specific Language(DSL,领域建模语言)来扩展HTML,还可以使用自定义标签和属性。
当然,这也是Web Component 背后的主要目标之一。前面我们已经提到过Angular 2是怎么使用这一新技术的以及为什么要使用它的原因。尽管AngularJS 1.x 中的模板很强大,但是还有很大的改进空间!Angular 2 中的模版吸取了上一个版本中的精华,解决了一些让人困惑的问题,增强了模板的功能。
假设我们创建了一个指令,允许用户通过标签的attribute 给它传递一个成员属性。在AngularJS 1.x 中,有以下三种不同的实现方法:
如果我们有一个user 指令,然后需要给它传递name 属性,有三种不同的方法可以实现(这里的意思看起来和上一段的末尾有一点重复,原文如此——译者注):第一种方法是传递一个字面量(在这个例子里面,也就是"literal");第二种方法是传递一个字符串,这个字符串可以当成表达式来执行(在这个例子里面,也就是"expression");第三种方法是在{{}}中传递一个表达式。应该使用哪一种语法完全由指令的具体实现来决定,这就使得指令的API 变成一团乱麻并且难以记忆。
在日常工作中,处理大量基于不同的设计方案而开发的组件是一件令人沮丧的事情。为了解决这些问题,我们需要引入一种通用的约定。但是,为了取得良好的结果并保持API 的一致性,需要整个社区达成一致。
Angular 2 为属性提供了特殊的语法来解决这个问题,属性值会在当前组件的上下文中执行,同时为传递字面量提供了不同的语法。
根据从AngularJS 1.x 中获得的经验,还有一件事情我们已经习惯了,那就是模板指令里面使用的微语法(microsyntax ),如ng-if、nf-for。举个例子,在 AngularJS1.x 中,如果需要遍历一个用户列表并展示用户姓名,我们可以这样做:
虽然这种语法看起来很直观,但是只有有限的工具能支持它。所以,Angular 2 引入了更明确的语法来解决这个问题,同时语义上也更丰富:
以上代码明确定义了一个(user)属性,这个属性将会在迭代(users)的上下文中创建。
但是,这种语法在输入的时候显得太冗长。所以,开发者可以使用以下简化语法,然后再编译成更冗长的形式:
文本编辑器和IDE 可以为改进型的新模板提供更高级的工具支持。在《迈向Angular2》第4 章Angular 2 中的组件和指令中,我们会讨论Angular 2 中的模板。
脏值检测
在关于WebWorker 的小节中,我们已经提到过:在WebWorker 实例化出来的其他线程上下文中运行digest 循环的时机。利用JavaScript 虚拟机的代码优化机制可以获得显著的性能提升,其中一种优化叫做内联缓存。但是AngularJS 1.x 中实现的digest循环内存使用效率不高,而且阻碍了这种优化过程。Angular 团队在这方面做了许多的研究,发现了提升digest 循环性能和效率的各种方法。这些发现推动了全新的脏值检测机制的开发。
为了获得更大的灵活性,Angular 团队把脏值检测机制提取了出来,并且与框架内核进行了解耦。这样一来就可以开发出不同的脏值检测策略,在不同的环境中可以采用不同的策略。
最终结果就是:Angular 2 中有两种内置脏值检测机制:
- 动态脏值检测:与AngularJS 1.x 中的脏值检测机制类似。用于不允许eval()的系统中,如CSP 插件和Chrome 插件。
- JIT 脏值检测:运行时动态生成脏值检测代码,允许 JavaScript 虚拟机进行深度代码优化。
《迈向Angular2》第4 章,会学习到新的脏值检测机制以及它们的配置方法。
到此,我们讨论了为什么需要使用最新版的JavaScript 语言;为什么要使用Web Component 和WebWorker;以及为什么不值得在1.x 版本中整合所有这些强大的工具。新框架层出不穷,好不好用只有自己踩过坑才会知道。