[转贴]有关Angular 2.0的一切

对Angular 2.0的策略有疑问吗?就在这里提吧。在接下来的这篇文章里,我会解释Angular 2.0的主要特性区域,以及每个变化背后的动机。每个部分之后,我将提供自己在设计过程中的意见和见解,包括我认为仍然需要改进设计的重要部分。

注意:本文所反映是2014年11月6日的状态记录。如果你在较长时间之后读到此文,请检查一下我设计上是否有所变更。

AngularJS 1.3

在开始讨论Angular的未来之前,我们先花点时间看看当前的版本。AngularJS 1.3是迄今为止最优的Angular版本,它是几周之前发布的。它提供了大量的bug修复,功能增强和性能提升。如果你正在使用Angular,会有升级的愿望。如果将要开始用Angular做新的项目,这也会是你想要使用的版本。这是一个强大而成熟的框架,已经摆在我们面前了。

评注

可能你现在会对AngularJS的未来有很多疑问。什么时候2.0会出来?1.x怎么办?会有从1.x到2.0的升级路径吗?AngularJS团队在回答这些问题上,可以做得更好一些,你应当鼓励他们这么做。我可以告诉你们的是,在Google内部,有1600个应用是基于Angular 1.2或者1.3构建的。所以,看起来Google对当前版本是会有很大兴趣的,也会需要支持它们一段时间。在ngEurope的Q&A环节中,Brad Green说在Angular 2.0的RTM版本发布之后,对Angular 1.3的支持会持续至少1.5-2年。我们也刚针对Angular 1.3的支持作了一些团队结构和领导的变更,即使是正在为Angular 2.0而努力,我们仍然保持了一个专有团队全职处理Angular 1.3。这个团队是由Pete Bacon Darwin领导的,我敢肯定你一定知道他在AngularJS上的丰富经验。我想要鼓励你们向Angular的引领者询问这些变化,并且一起设法完善官方的支持。

当2.0可用时,如果有人想要把Angular1.x的应用迁移到2.0,目前也没有什么可行的计划。我认为我们可以在这一块做些事。如果这对你来说也很重要,请大声说出来,当然要友善一点,但要让Angular团队知道这对你而言很重要,他们应当对此有所考虑,并且也有所规划。

Angular 2.0的动机

那么,你可能会想知道,为什么要做Angular 2.0呢?为什么一步跨到2.0,并且作了这么多不兼容变更?这一切都是很随意的吗?我能够处理少量变更,但我所听到的消息,在2.0中有很多较大的变更,它们真的合理吗?值得吗?

在深入特性细节之前,我很乐意花点时间来探讨一些较高层次的动机,关于2.0所带来的变化。我希望这能够对后续细节建立一个基本的认识,在此基础上可以作一些有意义的批评(其中有些我打算自己提供)。

性能

差不多五年前,当AngularJS刚创建出来的时候,它并不是给开发人员用的。它是一个工具,更倾向于给需要快速创建持久化HTML表单的设计人员用。随着时间推移,它作了改变以适应各种场景,开发人员也用它建造更多、更复杂的应用程序。Angular 1.x团队多年来一直努力增量化地改进设计,允许它适应现代Web应用程序需求的变更。然而,在所能做到的改进上,是有很大局限的,根源在于原始设计中的一些潜规则。很多这种限制,导致了当前的绑定与模板基础架构的性能问题。为了解决这些问题,需要新的策略。

变化的Web

从最初设想Angular所开始的五年中,Web有了明显的改变。比如说,5年前没有jQuery之类框架的帮助,是基本不可能建立一个合适的跨浏览器网站的。但是,当今的浏览器DOM实现不仅更加一致,而且这些实现更快了,也提供了与应用程序框架相关的新特性。

而且web还在继续变化……

虽然在过去几年中,发生了巨大的变化,但与未来1-3年相比,这些变化还是显得微不足道。在几个月内,ES6规范将定稿。如果我们觉得在2015年就能看到完全实现此规范的浏览器,并非不可能。今天的浏览器已经支持其中一些特性了,并且正在实现其他剩余部分。这意味着浏览器支持像module、class、lambda、generator之类东西。这些特性从根本上改变JavaScript的编程体验。但是,大的变化并不是只体现在JavaScript上,Web Components也喷薄欲出。术语Web Components通常是指四个相关的W3C规范:

  • 自定义元素,允许通过自定义标签来扩展HTML
  • HTML Imports,允许对各种资源的打包(HTML,CSS,JS等等)
  • 模板元素,允许在一个文档中包含inert HTML
  • Shadow DOM,允许对DOM和CSS的封装

通过组合这四种能力,web开发人员可以创建声明式的组件(自定义元素),并且是完全封装的(Shadow DOM)。这些组件可以描述它们自己的视图(模板元素),并且能很容易打包发布给其他开发人员(HTML Imports)。当这些规范在所有主流浏览器都可用的时候,我们就可能会看到开发人员的创造力爆发,作很多努力来创建可复用的组件,以解决常见问题,或者是弥补标准的HTML工具集所存在的不足(摘自Web Components与数据绑定)。今天,这已经变得可能,在Chrome和其他浏览器里,这些标准中有些已经实现,有些正在实现。未来显得很美好,对不对?只剩下一个问题了:当今的多数数据绑定框架尚未准备好应对这些。多数框架,包括Angular 1.x,包含一个数据绑定系统,它构建在一小部分已知的HTML元素和常用事件、行为的基础上。为了能让Angular开发人员享有Web Components,很需要有一个全新的数据绑定实现。

移动端

想想5年前……噢,计算的情景已经有了多么大的改变!现在到处都是手机和平板了!虽然Angular可以被用于创建移动应用,但它的理念并非为它们设计的。这包括了所有的东西,从我刚提到过的基本的性能问题,到它的路由的能力缺失,以及不能缓存预编译视图,甚至是过于普通的触摸支持。其中有些东西可以借助Angular 1.3来实现(比如说路由),但其余的需要根本的变更来修复。

易用性

老实说……AngularJS不是太容易学。是的,你选择了它,内心想着“这太美好,很简单,很魔幻!!!”然后开始建立自己的应用,发觉变成“TMD这什么啊!!??我不懂!!!”这种事我听得多了,甚至还有个直观的图用来描述它。如此种种,还是回头看看这个库最初的设计意图吧。比如,最开始是没有自定义指令的,它们都是硬编码的,然后,就有了一个用于添加指令的API。最开始是没有控制器的,然后……你懂的。这种绑死的特性,很多成为了现在的核心理念,导致了API的不优雅。如果Angular真想变的易学易用,那么,从一开始,它就必须对自己的核心特性有清晰的认识。对一个框架而言,如果把指令和控制器当作初始设计的一部分,肯定要比后面逐步拼凑起来要好几个数量级。

小结

了解了Angular的设计起源,以及Web和通用计算情景的逐步变化,很明显需要作一些变更了。事实上,如果不开始解决这些问题,Angular很可能在一年内就有被淘汰的风险。一个框架,如果没法跟Web Components协作,在移动端上一塌糊涂,还继续推进自己的非标准的module和class API,离死也不远了。Angular团队对这些问题的回答是一个新版本:Angular 2.0。它本质上是为了现代Web而对AngularJS的重新想象,并且融合过去五年所得到的各种认识。

评注

尽管我尚未涉及详细的部分,你已经可以发现AngularJS 2.0是与1.x大为不同的了。可能有人会问,这还是不是同一个框架了?我觉得这是个好问题。我之前提到,我认为Angular团队需要提供对1.x支持的具体时间表,到2.0的迁移路径,以及给企业一些指引,供当前决策或者是想要升级为2.0的计划用。对于充满技术思维的Angular团队来说,这可能是很无趣的任务,但我认为它们对社区而言,是有必要的,有帮助的,也是一种尊重。

Angular 2.0的特性与设计

现在,你已经对创建Angular 2.0的动机有了一点相关背景了,我们来看看一些关键的特性区域。

AtScript

AtScript是一门语言,它是ES6的超集,被用来编写Angular 2.0。它使用TypeScript的类型语法来表达可选类型,这可以用来做运行时的类型推断,而不是编译时的检测。它也使用了元数据注解来扩展语言。这里有一个示例,有些AtScript代码就长这样:

import {Component} from 'angular';
import {Server} from './server'; @Component({selector: 'foo'}) export class MyComponent { constructor(server:Server) { this.server = server; } }

在这里,我们在基线ES6代码上添加了一些AtScript附属物。示例顶部的import语句和class语法是直接从ES6里来的,没什么特别的,但是,看一下构造函数,注意server参数指定了一个类型。在AtScript中,这个类型是用于生成运行时类型推断的,引用也会存在于已知的位置,这样,一个框架,比如说依赖注入框架,可以定位到类型信息,并且使用它。也注意一下class定义上面的@Component语法,这是一个元数据注解。组件是一个普通的类,跟其他的一样。当你使用注解来装饰一个东西的时候,编译器会生成代码,初始化注解,并且储存在一个具体的位置,这样它可以被像Angular这样的框架访问到。考虑到这一点,这就是上面的代码转译成ES6语法之后的结果:

import * as rtts from 'rtts';
import {Component} from 'angular'; import {Server} from './server'; export class MyComponent { constructor(server) { rtts.types(server, Server); this.server = server; } } MyComponent.parameters = [{is:Server}]; MyComponent.annotate = [ new Component({selector: 'foo'}) ];

RTTS的意思是运行时类型系统(RunTime Type System),这是一个小型的关于运行时类型检测的推断库。在此,编译器插入一些代码,以便把server变量推断为类型Server。你也可以编写自定义的类型推断以使用结构化类型,或者使用临时的类型规则。当部署到生产的时候,编译器可以省略这些推断以提高性能。

一个好的事情是,与类型推断相独立,类型注解和元数据注解是能够被跟踪到的。所有这些注解会被翻译成非常简单的ES5兼容数据结构,存储在MyComponent函数自身上。这使得任意框架或者库都能很容易发现这些元数据并且使用它们。多年来,这已经在像.NET和Java这样的平台上被证明是很方便的工具。它也和Ruby的元编程功能有一些相似之处。实际上,当跟一个库组合的时候,注解可以被用于做元编程,Angular 2.0就是借此简化指令的创建的。稍后将进行更详细的讨论。

评注

我个人是喜欢AtScript的,但我已经是一个TypeScript的爱好者了,所以你可能会说我是有前提条件的。我知道有些开发人员抵制在JavaScript中加入类型,我没法责怪他们。我自己已经在类型系统上有过相当广泛的经验了,有些是好的,有些不好。AtScript有个有意思的地方,你可以只把类型语法当成向其他库提供元数据的某种简单方式,而完全不用它来作类型检测。我觉得AtScript最强大的特性之一是,在运行时拥有类型和元数据信息,可供框架利用,或者是在自己的元编程中使用。如果其他的解释型语言也加上这种特性的话,我是不会感到惊讶的。

也就是说,我持有保留意见。

我很乐于看到AtScript变得更正式些,我意思是说,我认为它应当从Angular团队自身中释放出来,它应当有自己的发言权,Angular作为它的一个重要客户。应当至少有几个开发人员全职围绕AtScript工作,实现功能,修复bug,提升代码生成,构建工具等等,同时也应当有一个长期支持计划。当一个开发人员或者团队选择一种语言来编写他们应用的时候,他们所作出的是一种重大投资。我乐于看到Google能够为了未来,在AtScript上作出相当的投资。

关于AtScript,还有另外个问题,是跟Dart相关的。Dart是Google开发的另一种语言。它跟某种简单的解释性语言有所不同,是因为它有自己的运行时和基础类库。结果就是,Dart拥有自己的API,用于DOM处理,集合,事件,正则表达式等等。这些API在它们自己的领域中都很优秀,但跟已有的JavaScript代码不兼容。由于这种阻抗不匹配,Dart和外界的任何通讯都必须通过一个特殊的编组API来完成。所以,虽然从技术上可以调用现有的JavaScript库,一般来说不太实用。对AngularJS来说,性能上的损耗将是不可接受的。所以,Google创建了Angular Dart,一种用Dart重新思考过的AngularJS版本。

问题解决了……好吧,可能没有。

现在,就有了两个Angular的版本,要在里面修改bug,实现新特性,发布,等等,使用不同的语言编写,由不同的团队维护。所以,解决了一个问题,却带来了更多问题。

现在你可能有疑问了:这跟AtScript有什么关系呢?

Angular 2.0的想法是把Angular和Angular Dart统一起来。一个团队在一个代码库上工作,要比两个团队在两个代码库上工作好多了。AtScript能在这个事情上起作用,因为它是在Traceur上面实现的,这个东西可扩展性很好。所以,Angular团队能够用AtScript编译出JavaScript和Dart两个版本。

太棒了!那么,问题在哪里呢?

记得我提到过Dart在DOM之类的东西上有不同的对象模型,这些东西就不是简单转译代码所能解决的了。因此,Angular 2.0的构建过程实际上就会复杂一些了。当开发Angular的时候,必须创建不同的门面(facade)以屏蔽JavaScript和Dart之间的API差异。然后编译器使用对应的门面来编译成每种指定的语言。这个事情在技术上肯定是令人印象深刻了,但是,却大大提高了想要转向Angular 2.0的准入门槛。值得注意的是,这方面的发展还处于试验阶段,这个问题可能会有其他的解决方案。我知道你们中的很多人已经转向了Angular,并且很珍惜这种经验。Angular团队也很珍惜它们,我们正在深度思考如何去改进这些,不过,到目前为止,还不是很理想。

注意:Angular 2.0是使用AtScript编写的,但这并不意味着你就需要用AtScript编写你的应用,或者为了使用Angular 2.0,要学AtScript的什么东西。你可以很轻松地使用TypeScript,ES6,ES5,CoffeeScript……随便什么喜欢的东西来写。目前来说,如果利用AtScript的话,能够获得最佳的Angular体验,因为它能够从语言原语自动生成元数据,不过,最终它还是会翻译成简单的ES5。最终的ES5在概念上某种程度类似于Angular 1.x里面的DDO对象,但在此情况下,它是被生成给任意JavaScript函数使用的,而不是某种指令相关的技术,需要用特殊的注册API来编写。

依赖注入

Angular 1.x的核心特性之一是依赖注入(DI,Dependency Injection)。通过DI,你可以很容易地在软件开发过程中遵循“分而治之”的实践。复杂的问题可以根据其角色和职责进行概念化,然后表示成对象,共同协作以完成最终目标。使用这种方式解构的大型(或者小型)系统可以通过使用DI框架在运行时进行组装。这种系统通常是容易测试的,因为结果的设计更加模块化,也允许了更容易的组件隔离。当然,这一切在Angular 1.x中都是可以的,不过有一些问题。

困扰1.x DI实现的第一个问题是由压缩(minification)引起的。鉴于DI依赖于从函数解析参数名,本质上是把它们当作字符串令牌,而在压缩过程中,这些名称会被改变,就不再匹配于注册的服务、控制器和其他组件了。结果就是应用挂了。为了使得DI对压缩友好,添加了一个API,但它缺乏原始的优雅。在.NET和Java的世界中,先进的服务端DI框架里存在更多特性,1.x的实现主要就缺乏这些东西。欠缺的特性导致开发人员受到约束,两个大的例子是:生命周期/作用域的控制,以及子注射器。

注解

通过AtScript,我们引入了一种广义的将元数据附加到任意函数的机制。同时,AtScript元数据格式是不怕压缩的,也容易使用ES5手工编写。这使得它能够出色地支撑一个DI库,提供其所需要用于构造对象实例的信息。不必见怪,这就是新DI的运作方式。

当DI需要实例化一个类(或者调用一个函数)的时候,会检测一下,看看它上面有没有带附属的元数据。回顾一下上面从AtScript转译出来的代码:

MyComponent.parameters = [{is:Server}];

如果新DI发现了parameters值,会用它来判断将要尝试调用的函数的依赖项。在本例中,它可以得知仅有一个类型为Server的参数。所以它会获取一个Server的实例,并且在调用这个函数之前传进去。你也可以显式提供一个特定的Inject注解给DI用,这会覆盖parameter数据。如果你在使用一种不能自动生成parameter元数据的语言,也很容易支持,下面就是用原生ES5代码写的样子:

MyComponent.annotate = [new Inject(Server)];

这个的运行时效果跟parameter数据是一样的。值得注意的是,你可以使用任意东西当作注入令牌,所以可以这样:

MyComponent.annotate = [new Inject('my-string-token')];

只要你在DI上配置过能映射到'my-string-token'的东西,它就能运行。也就是说,推荐的使用方式是通过构造函数的实例,正如我之前所有的例子所示。

实例作用域

在Angular 1.x中,DI容器中的所有实例都是单例。在Angular 2.0中,默认也是这样。为了获得不同的行为,你需要使用Service,Provider,Constant等等。那都挺容易让人迷惑的。幸运的是,新DI拥有一个新的,更通用,更强大的特性。它现在有实例作用域控制了。所以,如果你希望每次请求的时候,DI都创建一个类的新实例,可以这么做:

@TransientScope
export class MyClass { ... }

当你组合子注射器来创建自己的作用域标识符的时候,这会更加强大……

子注射器

子注射器是一个主要的新特性。子注射器从其父项那里继承到所有父项的服务,但能够在子级别上覆盖它们。当它与作用域标识符组合使用的时候,你可以很轻松地在系统中调用到特定类型的对象,这些对象应当在不同作用域中被覆盖,这非常强大。新的路由有一个“子路由”的功能,就是使用它的一个例子。在内部,每个子路由创建自己的子注射器,这使得路由的每个部分能够从父路由继承服务,或者在不同的导航场景中覆盖这些服务。

注意:自定义作用域和子注射器会被认为是对注射器的中高级用法。我不希望太多的应用代码用到它。但是,既然它在Angular的内部被使用到了,如果你需要类似功能,也可以用。

更多……

在新DI中,还有一些其他特性,比如provider(自定义函数,用于提供一个注入值),懒注入(指定你所期望注入的东西,但又不立即需要,稍后才要),还有基于promise的异步注入(注入一个promise,可以从中获取异步的依赖项)。

评注

从个人角度,我非常喜欢新DI。我又有偏见了,因为我用DI好多年了,在我创建的其他UI框架中,它也是核心组件。新DI在Angular 2.0中扮演了很重要的角色,像子注射器等功能带来了巨大的变更。现在这个功能有了,它能够被模板引擎和路由利用,这两者都有创建作用域和隔离不同服务的需求。

然后我们就来到了一个从Angular中移除的重要特性:$scope。不过,虽然$scope自身被移除了,它的有些特性还在。这些特性被作为此设计的一个部分,重新换了个位置,也有所提升。你可能会被$scope的缺失搞得措手不及,但新的设计既简化了Angular内部的东西,也简化了提供给你,开发人员的东西。我提到这些,是因为DI的有些新功能,比如说子注射器,与$scope中之前的一些功能重叠了。在这个情况下,我认为新DI系统拿出来的是一个更好的解决方案。它更加通用,所以不但能解决Angular的内部需求,还给你们开放了很多种可能性。

不幸的是,玫瑰带着刺。我们来讨论下一些其他问题。在Angular 1.x中,还有个相关功能我没有提到:module,你可能想知道它的位置在哪。Angular 2.0的方案是吸收ES6中关于module的标准。在Angular的之前版本中,处理模块的方式是Angular特有的方式。五年前,当Angular刚开始构思时,并没有用于完成此事的标准方式。今天,事情不同了,已经有了一个明确路径。这当然是一种不兼容升级,如果有人要作迁移的话,需要对代码重新作点调整。要作这么一种不兼容变更,是很恶心,但这就是Web的变化影响框架的一个实例,如果这事现在不解决,2.0将面临被边缘化的风险。

关于DI,还有另外一个坑,特别是如果你在用ES5写代码的话。Angular 2.0依托作为元数据的注解,支持了基于类的设计。类和注解的语法在ES5中并不太好,事实上,ES5压根就没这些语法。你可以使用原型之类来表示一切,但就没有AtScript甚至ES6或者TypeScript那么清晰了,它们可是有类和静态的类成员的。我想知道能不能为不准备迁移到ES6的开发人员做点什么,也许是一个简单的可选降级库,给出一种简单的方式来创建带元数据的类?可能会类似于Angular 1.x中的DDO对象,但是更通用,这样能创建任意的类和元数据。我很想听听你对这个想法的意见,或者其他可能会解决ES5开发问题或对迁移能有所提升的主意。

模板与数据绑定

如果你已经看了这么多,一定属于对Angular 2.0非常好奇的,感谢花了这么多时间。我们还有一条路要走,现在我们要进入真正有意思的地方了:模板和绑定。我打算把它们放在一起讨论,虽然从技术上看,数据绑定系统是与模板系统分离的,你在编写应用的时候却会感觉它们是一个整体。所以,我觉得把它们拿到一起来说会比较好。

我们先从理解视图到屏幕的显示过程,然后一点一点地看。本质上,你是从一段HTML片段开始的,这会存在于一个