@kungfuboy
2016-12-19T02:07:02.000000Z
字数 22225
阅读 2106
小源
在Angular1.X版本的年代,我们通过script标签引入一个Angular脚本就能工作,但在Angular2不能这么干,它是面向未来的框架,有点超前,要求浏览器支持ES6+,我们现在要尝试的话,需要加一些垫片来抹平当前浏览器与ES6的差异:
angular2 polyfills - 为ES5浏览器提供ES6特性支持,比如Promise等。systemjs - 通用模块加载器,支持AMD、CommonJS、ES6等各种格式的JS模块加载typescript - TypeScript转码器,将TypeScript代码转换为当前浏览器支持的ES5代码。reactive extension - javascript版本的反应式编程/Reactive Programming实现库,被打包为systemjs的包格式,以便systemjs动态加载。angular2 - Angular2框架,被打包为systemjs的包格式,以便systemjs动态加载模块。不过,如果我们将上述几个文件打包成一个
.js文件,就能像Angular1.X一样通过脚本引入工作了。
import {Component} from "angular2/core";import {bootstrap} from "angular2/platform/browser";// 我们从angular2模块库中引入了两个类型: Component类和bootstrap函数。// 别问我怎么知道这两个类的位置,我也不知道!@Component({selector:"my-app",template:"<h1>Hello,World</h1>"})class myApp{}bootstrap(EzApp);
我们来逐行分析:
@Component告诉Angular2:你将要解析一个组件。selector属性,告诉Angular2框架,这个组件渲染到哪个DOM对象上。template属性,告诉Angular2框架,使用什么模板进行渲染。bootstrap函数的作用是通知Angular2:请将组件渲染到DOM上!@Component给类myApp附加的元信息,也被称为注解或者Annotation。这个东西,就功能来看,有点像是其他语言中的装饰符,但是ES6并没有装饰符的概念,这实际上是转码器的一个特性:注解。
给一个类加注解,等同于设置这个类的annotations属性:
// 注解写法@Component({selector:"my-app"})class myApp{...}// 等效于下面的写法class myApp{...}myApp.annotations = [new Component({selector:"my-app"})];// 注解在编译转码时,仅仅被放在类对象的annotation属性里,编译器并不进 行解释展开 —— 这个解释的工作是Angular2框架完成的
这通常不是编译器的功能,是Angular2团队特别要求的,因此我们可以看到,配置文件systemjs在使用TypeScript转码时默认是打开注解的:
System.config({transpiler: 'typescript',typescriptOptions: { emitDecoratorMetadata: true },});
使用Component注解的selector属性来告诉Angular2,当编译、链接模板时,如果 看到这个选择符,就实例化一个组件对象。
selector属性使用CSS选择符的匹配规则。
// 匹配<my-app>...</my-app>@Component({selector:"ez-one",template:"TAGNAME-SELECTOR"})class EzOne{}// 匹配<any class="ez-two">...</any>@Component({selector:".ez-two",template:"CSSCLASS-SELECTOR"})class EzTwo{}// 匹配<any ez-three>...</any>@Component({selector:"[ez-three]",template:"ATTR-SELECTOR"})class EzThree{}// 匹配<any ez-four='123'>...</any>@Component({selector:"[ez-four=123]",template:"ATTRVAL-SELECTOR"})class EzFour{}
在ES6中,使用一对``符号就可以定义多行字符串,这比使用双引号要轻松不少。
@Component({template : `<h1>hello</h1><div>...</div>`})
使用templateUrl属性可以引入外部模板,这可以让组件看起来更清爽一些。
@Component({templateUrl : "ezcomp-tpl.html"})
@Component({styles:[`h1{background:#4dba6c;color:#fff}`]})
显然我们会更倾向于这种,不然sass上哪去写去?
@Component({styleUrls:["ez-greeting.css"]})
属性是组件暴露给外部世界的调用接口,调用者通过设置不同的属性值来定制组件的行为与外观。在Angular2中为组件增加属性接口非常简单,只需要在Component的properties属性中声明组件的成员变量就可以了:
//myCard@Component({properties:["name","country"]})···// myApp@Component({directives : [myCard],template : `<ez-card [name]="'雷锋'" [country]="'中国'"></ez-card>`})
与属性相反,事件从组件的内部流出,用来通知外部世界发生了一些事情。在Angular2中为组件增加事件接口也非常简单:定义一个事件源/EventEmitter,然后通过Component的events接口暴露出来:
@Component({events:["change"]})class myCard{constructor(){this.change = new EventEmitter();}}
上面的代码将组件myCard的事件源change暴露为同名事件,这意味着在调用者 myApp组件的模板中,可以直接使用小括号语法挂接事件监听函数:
// myApp@Component({template : "<ez-card (change)="onChange()"></ez-card>"})
组件,就是指令
在模板中使用可以{{表达式}}的方式绑定组件模型中的表达式,当表达式变化时,Angular2将自动更新对应的DOM对象:
@Component({selector:"ez-app",template: `<div><h1>{{title}}</h1><p>{{content}}</p><div><span>{{date}}</span> 来源:<span>{{source}}</span></div></div>`})
在模板中,也可以使用一对中括号将HTML元素或组件的属性绑定到组件模型的某个表达式,当表达式的值变化时,对应的DOM对象将自动得到更新。
@Component({selector:"my-app",template:`<h1 [style.color]="color">Hello,Angular2</h1>`})// 等价于@Component({selector:"ez-app",template:`<h1 bind-style.color="color">Hello,Angular2</h1>`})
除了用中括号绑定之外,也可以用bind-前缀来绑定。
需要注意的是,属性的值总是被当做调用者模型中的表达式进行绑定,当表达式变化时,被调用的组件自动得到更新。如果希望将属性绑定到一个常量字符串,别忘了给字符串加引号,或者去掉中括号:
//错误,Angular2将找不到表达式 Hello,Angular2@Component({template:`<h1 [textContent]="Hello,Angular2"></h1>`})//正确,Angular2识别出常量字符串表达式 'Hello,Angular2'@Component({template:`<h1 [textContent]="'Hello,Angular2'"></h1>`})//正确,Angular2识别出常量字符串作为属性textContent的值@Component({template:`<h1 textContent="Hello,Angular2"></h1>`})
在模板中为元素添加事件监听很简单,使用一对小括号包裹事件名称,并绑定到表达式即可。
@View({template : `<h1 (click)="roulette()">ROULETTE</h1>`})// 等价于@View({template : `<h1 on-click="roulette()">ROULETTE</h1>`})
有时模板中的不同元素间可能需要互相调用,Angular2提供一种简单的语法将元素映射为局部变量:添加一个以#或var-开始的属性,后续的部分表示变量名, 这个变量对应元素的实例。
在下面的代码示例中,我们为元素h1定义了一个局部变量v_h1,这个变量指向该元素对应的DOM对象,你可以在模板中的其他地方调用其方法和属性:
@Component({template : `<h1 #v_h1>hello</h1><button (click) = "v_h1.textContent = 'HELLO'">test</button>`})
如果在一个组件元素上定义局部变量,那么其对应的对象为组件的实例:
@Component({directives:[EzCalc],template : "<ez-calc #c></ez-calc>"})
在上面的示例中,模板内的局部变量c指向EzCalc的实例。
我们可以使用样式绑定的方法设置单一样式。但如果要同时设置多个样式值,可以使用 NgStyle指令,将内联样式绑定到组件的属性上。
NgStyle指令的选择符是[ngStyle],下面的示例将div元素的样式绑定到组件的 styles属性:
<div [ngStyle]="styles">...</div>
styles属性则当然是在constructor()里定义:
this.styles = {"color" : "red","font-style" : "italic","background-color" : "black"}//等价于this.styles = {color: "red"fontStyle : "italic",backgroundColor : "black"}
在Web App中,我们通常采用动态添加或删除样式类的方法,来改变DOM元素的外观表现。
当然,我们可以HTML元素的class属性绑定到组件实例的属性上,然后通过改变实例的属性, 实现动态修改HTML元素的样式类:
<div [class]="cls">...</div>
不过,如果一次要删除或添加多个.class,Angular2内置的NgClass指令会更简单。NgClass指令的选择符是ngClass,下面的示例将div元素的class属性绑定到组件的cns属性:
<div [ngClass]="cns">...</div>
cns属性是一个JSON对象,每个键代表样式类名,对应的值为true时表示向HTML元素 添加该样式类,为false时表示删除该样式类。如果cns的值如下:
this.cns = {colorful:true,italic : false,bold : true}
如此,该模板渲染之后的结果是:
<div class="colorful bold">...</div>
有时我们需要模板的一部分内容在满足一定条件时才显示,这是指令NgIf发挥作用的场景,它评估属性ngIf的值是否为真,来决定是否渲染template元素的内容:
@Component({template : `<!--根据变量trial的值决定是否显示图片--><template [ngIf]="trial==true"><img src="ad.jpg"></template><!--以下是其他必须显示的元素--><pre>...` })
Angular2同时提供了两种语法糖,让NgIf写起来更简单,下面的两种书写方法和上面的正式语法是等效的:
//使用template attribute<img src="ad.jpg" template="ngIf tiral==true">//使用*前缀<img src="ad.jpg" *ngIf="tiral==true">
看起来,显然*ngIf的书写方法更亲切一些,不过无论采用哪种书写方法,都将转换成上面的正式写法,再进行编译。
如果组件的模板需要根据某个表达式的不同取值展示不同的片段,可以使用NgSwitch系列指令来动态切分模板。NgSwitch包含一组指令,用来构造包含多分支的模板:
NgSwitch指令可以应用在任何HTML元素上,它评估元素的ngSwitch属性值,并根据这个值决定应用哪些template的内容(可以同时显示多个分支):
<ANY [ngSwitch]="expression">...</ANY>
NgSwitchWhen指令必须应用在NgSwitch指令的子template元素上,它通过属性ngSwitchWhen指定一个表达式, 如果该表达式与父节点的NgSwitch指令指定的表达式值一致,那么显示这个template的内容:
<ANY [ngSwitch]="..."><!--与变量比较--><template [ngSwitchWhen]="variable">...</template><!--与常量比较--><template ngSwitchWhen="constant">...</template></ANY>
NgSwitchDefault指令必须应用在NgSwitch指令的子template元素上,当没有NgSwitchWhen指令匹配 时,NgSwitch将显示这个template的内容:
<ANY [ngSwitch]="..."><template ngSwitchDefault>...</template></ANY>
也就是说,整个组合的逻辑是这样的:
@Component({selector : "myApp",properties:["gender"],template : `<div [ngSwitch]="gender"> //判断开始<template ngSwitchWhen="Male"> //有匹配执行<img src="img/male-ad.jpg" class="banner"></template><template ngSwitchWhen="Female"> //有匹配执行<img src="img/female-ad.png" class="banner"></template><template ngSwitchDefault> //默认或无匹配执行<h1>Learn Something, NOW!</h1></template></div>`})
如果希望利用一组可遍历的数据动态构造模板,那么应当使用NgFor指令。
NgFor指令应用在template元素上,对ngForOf属性指定的数据集中的每一项实例化一个template的内容:
<template ngFor [ngForOf]="items" ><li>----------</li></template>
如果items数据集有3条记录,那么会生成3个li对象,就像这样:
<li>----------</li><li>----------</li><li>----------</li>
不过这没多大用。
好在我们还可以为数据集的每一项声明一个局部变量,以便在模板内引用:
<template ngFor [ngForOf]="items" #item><li>{{item}}</li></template>
假如items数据集是一个数组:["China","India","Russia"],那么现在生成的结果就有点用了:
<li>China</li><li>India</li><li>Russia</li>
有时还需要数据项在数据集中的索引,我们也可以为数据集的每一项的索引声明一个局部变量,以便在模板内引用:
<template ngFor [ngForOf]="items" #item #i="index"><li>[{{i+1}}] {{item}}</li></template>
现在生成的结果更规矩了:
<li>[1] China</li><li>[2] India</li><li>[3] Russia</li>
与NgIf类似,Angular2也为NgFor提供了两种语法糖:
//使用template attribute<ANY template="ngFor #item of items;#i=index">...</ANY>//使用*前缀<ANY *ngFor="#item of items;#i=index">...</ANY>
毫无疑问,应当尽量使用*ngFor的简便写法,这可以提高模板的可读性。
有时候需要在表现层对数据进行处理,Angular2的管道就派上用场了,管道的作用的是:在模板中对输入数据进行变换,并输出变换后的结果。在模板中,使用|来调用一个管道操作,使用:来向管道传入参数。
例如:{{ data | <pipename>:<arg1>:<arg2> }}
Angular2中已经有了许多预置的管道来对常见数据类型进行处理:
DatePipe - 对日期/时间数据进行格式变换。
{{ day | date:'yyMMdd'}}
JsonPipe - 将JSON对象转换为字符串,其实现基于JSON.stringify()。
{{ {x:1,y:2} | json }}
PercentPipe - 将数值转换为百分比。
{{ 1.23456 | percent:'1.2-3' }}
SlicePipe - 提取输入字符串中的指定切片
在模板中使用slice来引用SlicePipe。第一个参数指定切片的起始索引, 第二个参数指定切片的终止索引的下一个。
<!--结果:'123'-->{{ '01234567890' | slice:1:4 }}
UpperCasePipe - 将输入字符串变换为大写
在模板中使用uppercase来引用UpperCasePipe。
<!--结果:THIS IS A DEMO-->{{ "this is a demo" | uppercase }}
LowerCasePipe - 将输入字符串变换为小写
在模板中使用lowercase来引用LowerCasePipe。
<!--结果:what a wonderful world-->{{ "WHAT A WONDERFUL WORLD" | lowercase }}
管道可以联级
和实现一个组件类似,一个管道也是具有特定元数据的类:
@Pipe({name:'ezp'})class EzPipe{...}
Pipe注解为被装饰的类附加了管道元数据,其最重要的属性是name,也就是 我们在模板中调用这个管道时使用的名称。上面定义的管道,我们可以在模板中 这样使用:{{ data | ezp }}。
管道必须实现一个预定的方法transform(input,args),这个方法的input参数代表输入数据,args参数代表输入参数,返回值将被作为管道的输出。
下面的示例简单地将输入数据与所有参数拼接在一起:
@Pipe({name:'ezp'})class EzPipe{transform(input,args){return input + " " +args.join(" ");}}
在组件的模板中使用自定义管道之前,需要预先声明一下,以便Angular2注入。 使用Component注解的pipes属性进行声明:pipes:[EzPipe]。
现在,我们就可以使用这个自定义管道了:
<!--结果:"call join mary linda"-->{{ "call " | ezp:'john':'mary':'linda' }}
我们之前了解的管道,包括Angular2预置的管道,以及我们自己实现的管道,都有一个特点, 就是输出仅仅依赖于输入,这样的管道,在Angular2中被称为无状态管道/Stateless Pipe。
对于无状态管道,当输入没有变化时,Angular2框架不会重新计算管道的输出。但也许有些时候,我们希望即使输入没有变化,也持续地检测管道的输出。例如,我们设计了一个倒计时管道,向 它输入一个秒数,会自动多次输出直至0秒:
{{ 10 | countdown }}
实现countdown的逻辑很简单,记录起始值,然后开一个1秒1次的计时器,逐次减至0秒即可。
关键在于,默认情况下,Angular2框架仅仅执行一次管道的transform()方法,我们需要 使用Pipe注解的pure属性值为false,要求Angular2框架在每个变化检查周期都执行 管道的transform()方法:
@pipe({name:"countdown",pure : false})class EzCountdown{...}
很显然,countdown管道的输出不仅依赖于输入,还依赖于其内部的运行状态。因此,这样 的管道,在Angular2中被称为有状态管道/Stateful Pipe。
需要指出的是,管道的有状态与无状态的区别,关键在于是否需要Angular2框架在输入不变的 情况下依然持续地进行变化检测,而不在于我们通常所指的计算的幂等性 - 即同样的输入 总是产生同样的输出。
例如,一个计算累加值的管道,在传统的概念中,应当被视为有状态的,因为它对于同样的输 入,会累加之前记录的总和,因此会产生不同的输出。但是,在Angular2中,它依然被视为无状态的,因为,它的一次输入不会产生多次输出。
AsyncPipe是Angular2框架预置的一个有状态管道,它的输入是一个异步对象:Promise对象、Observable对象或者EventEmitter对象。
每当异步对象产生新的值,AsyncPipe会返回这个新的值,因此,AsyncPipe需要Angular2框架持续进行变化检测,它的Pipe注解的pure属性值为false。
我们可以使用AsyncPipe来重写之前的EzCountdown管道,让它返回一个Observable:
@Pipe({name : "countdown"})class EzCountdown{transform(input){var counter = input;return new Observable(o => {setInterval(_ => {o.next(counter);counter--;if(counter<0) o.complete();},1000);});}}
你注意到现在EzCountdown是一个无状态的管道了,它返回一个Observable对象,我们在模板中使用AsyncPipe来继续处理这个对象:
{{ 10 | countdown | async }}
简单地说,Observable和EventEmitter适合用多次返回结果的异步应用场景,而Promise则适合于仅返回一次结果的异步应用场景。
NgForm指令为表单元素/form建立一个控件组对象,作为控件的容器。而NgControlName指令为则为宿主input元素建立一个控件对象,并将该控件加入到NgForm指令建立的控件组中:

通过使用#符号,我们创建了一个引用控件组对象(注意,不是form元素!)的局部变量f。 这个变量最大的作用是:它的value属性是一个简单的JSON对象,键对应于input元素的 ng-control属性,值对应于input元素的值:
NgForm指令和NgControlName指令都包含在预定义的数组变量FORM_DIRECTIVES中,所以我们在组件注解的directives属性中直接声明FORM_DIRECTIVES就可以在模板中直接使用这些指令了:
//angular2/src/common/forms/directives.tsexport const FORM_DIRECTIVES: Type[] = CONST_EXPR([NgControlName,NgControlGroup,NgFormControl,NgModel,NgFormModel,NgForm,NgSelectOption,DefaultValueAccessor,NumberValueAccessor,CheckboxControlValueAccessor,SelectControlValueAccessor,NgControlStatus,RequiredValidator,MinLengthValidator,MaxLengthValidator]);
如前所述,NgControlName指令必须作为NgForm或NgFormModel的后代使用,因为这个指令需要将创建的控件对象添加到祖先(NgForm或NgFormModel)所创建的控件组中。
NgControlName指令的选择符是[ngControl],这意味着你必须在一个HTML元素上 定义ngControl属性,这个指令才会起作用。
NgControlName指令为宿主的DOM对象创建一个控件对象,并将这个对象以ngControl属性 指定的名称绑定到DOM对象上:
<form #f="ngForm"><input type="text" ngControl="user"><input type="password" ngControl="pass"></form>
在上面的代码中,将创建两个Control对象,名称分别为user和pass。
除了使用控件组获得输入值,NgControlName指令可以通过ngModel实现模型与表单的双向绑定:
<form><input type="text" ngControl="user" [(ngModel)]="data.user"><input type="password" ngControl="pass" [(ngModel)]="data.pass"></form>
ngModel即是NgControlName指令的属性,也是它的事件,所以下面的两种写法是等价的:
<input type="text" ngControl="user" [(ngModel)]="data.user">//等价于<input type="text" ngControl="user" [ngModel]="data.user" (ngModel)="data.user">
NgControlGroup指令的选择符是[ng-control-group],如果模板中的某个元素具有这个属性, Angular2框架将自动创建一个控件组对象,并将这个对象以指定的名称与DOM对象绑定。
控件组可以嵌套,方便我们在语义上区分不同性质的输入:

和NgControlName指令一样,NgControlGroup指令也必须作为NgForm或NgFormModel的后代使用,因为这个指令需要将创建的控件组对象添加到祖先(NgForm或NgFormModel)所创建 的控件组中。
与NgControlName指令不同,NgFormControl将已有的控件/Control对象绑定到DOM元素上。当需要对输入的值进行初始化时,可以使用NgFormControl指令。
下面的代码中,使用NgFormControl指令将DOM元素绑定到组件EzComp的成员 变量movie上,我们需要在构造函数中先创建这个Control对象:
@View({//将输入元素绑定到已经创建的控件对象上template : `<input type="text" [ngFormControl]="movie">`})class EzComp{constructor(){//创建控件对象this.movie = new Control("Matrix II - Reload");}}
控件/Control是Angular2中对表单输入元素的抽象,我们使用其value属性,就可以获得对应的输入元素的值。
与NgControlName指令的另一个区别是,NgFormControl不需要NgForm或NgFormModel的祖先。
NgFormModel指令类似于NgControlGroup指令,都是为控件提供容器。但区别在于,NgFormModel指令将已有的控件组绑定到DOM对象上:
@View({template : `<!--绑定控件组与控件对象--><div [ngFormModel]="controls"><input type="text" ngControl="name"><input type="text" ngControl="age"></div>`})class EzComp{constructor(){//创建控件组及控件对象this.controls = new ControlGroup({name :new Control("Jason"),age : new Control("45")});}}
NgFormModel指令可以包含NgControlGroup指令,以便将不同性质的输入分组。
指令是Angular对HTML进行扩展的基本手段。与Angular1.x不同,在Angular2中,指令被明确地划分为三种类型:
- 组件 - 组件其实就是带有模板的指令
- 属性指令 - 属性指令用来改变所在元素的外观或行为,例如NgClass和NgStyle指令
- 结构指令 - 结构指令用来向DOM中添加或删除元素,例如NgIf和NgFor指令。
组件使用Component注解来装饰组件类,而属性指令和结构指令则使用Directive注解 来装饰指令类。
Directive注解最重要的属性是selector,它指定了触发Angular2框架生成指令实例 的CSS选择符。下面的示例定义了一个指令EzDirective:
@Directive({selector:"[ez-h]"})class EzHilight{...}
模板中具有ez-h的元素,Angular2框架都将为其生成一个EzDirective类实例。例如, 下面的模板中,框架姜维div元素实例化EzHilight:
<div ez-h>...</div>
很显然,我们需要在EzDirective类的实现中进行DOM操作,这需要告诉Angular2框架向我们 注入ElementRef对象,其nativeElement属性就是对应的DOM对象:
class EzHilight{constructor(@Inejct(ElementRef) elref){this.el = elref.nativeElement; //获取指令所在的DOM元素this.el.styles.color = "red"; //进行DOM操作}}
如果要在组件中使用自定义指令,需要在Component注解中设置directives属性:
@Component({selector : "ez-app",template : "<div ez-h>...</div>",directives : [EzHilight]})class EzApp{}
很显然,在定义组件模板时,我们通常会给属性设定一个值,比如,我们希望以下的模板片段中,将指令所在的DOM对象的背景设置为指定颜色:
<div [ez-h]="'black'">...</div>
通过使用Directive注解的inputs属性,我们可以将DOM对象的属性映射到指令对象的属性,例如,对于下面定义的指令:
@Directive({selector : "[ez-h]",inputs : ["bgColor:ez-h"]})class EzHilight{...}
当在模板中使用这个指令时,EzHilight对象的bgColor属性自动绑定到模板中div元素的ez-h属性的值。对于指令而言,这是一个输入,每当ez-h发生变化,Angular2 将自动设置EzHilight的bgColor属性。
我们可以使用ES6中的setter,在EzHilight中捕捉每个变化的时刻:
class EzHilight{set bgColor(v){this.el.style.background = v;}}
修改示例代码,为EzHilight指令增加一个新的属性color。
如果指令的实现需要监听所在DOM元素的事件,可以使用Directive注解的host属性。
下面的示例中,指令将监听所在DOM元素的两个事件 - click和mouseover:
@Directive({selector : "[ez-h]",host : {'(click)':'onMyClick()','(mouseover)':'onMyMouseOver()'}})class EzHilight{...}
你看到,host属性的值应当是一个JSON对象,其键为一对小括号包裹的事件名称,书写方法与在模板中一致;值为事件处理表达式,通常是对指令类中方法的调用。例如:
class EzHilight{onMyClick(){...}onMyMouseOver(){...}}
在EzHilight指令的实现中,我们是直接通过ElementRef对象的nativeElement属性来直接操作浏览器DOM的,不过Angular2其实不希望我们这么做,因为这将使我们的代码与浏览器纠缠不清,有违Angular2的跨平台本意 —— 换句话说,这么直接操作DOM的做法,是反模式的。
在Angular2中,引入了渲染器/renderer的概念,它定义了一组规范的接口Renderer,对于不同的平台,有不同的实现。比如,对于浏览器,对应的Renderer实现是DomRenderer。
在指令的构造函数中,我们可以要求Angular2框架注入当前使用的渲染器对象:
class EzHilight{constructor(@Inject(ElementRef) el,@Inject(Renderer) renderer){this.el = el;this.renderer = renderer;}}
Angular2希望我们使用Renderer来代替直接的DOM操作,这将保证我们的代码获得跨平台特性。现在我们使用Renderer的setElementStyle()方法来修改样式:
class EzHilight{set bgColor(v){this.renderer.setElementStyle(this.el,"background",v);}}
在Angular2中,服务用来封装可复用的功能性代码。比如Http服务,封装了ajax请求的细节,在不同的组件中,我们只需要调用Http服务的API接口就可以给组件增加 ajax请求的功能了:

Angular2中实现一个服务非常简单直接 : 定义一个类,然后,它就是服务了:
class EzAlgo{add(a,b){return a+b;}sub(a,b){return a-b;}}
上面的代码定义了一个相当弱智的算法服务EzAlgo,它有两个API - add()用来 计算两个数的和,sub()用来计算两个数的差 。在示例中,组件EzApp依赖于这个 服务:

在前一节的示例代码中,组件EzAlgo直接在构造函数中实例化了一个EzAlog对象, 这造成了EzApp和EzAlgo的强耦合,我们可以使用Angular2的注入器/Injector进行 解耦:

注入器就像婚姻介绍所,男方在婚介所登记心仪的女性特点,约好见面地点,然后, 坐等发货即可。比如上图:
EzApp组件(男方)使用Component注解的appInjector属性向Angular2框架(婚介所)声明其依赖于EzAlgo(登记心仪的女性特点),并在其构造函数的参数表中使用Inject注解声明注入点(约好见面地点),然后,剩下的事儿Angular2(婚介所)就办了:
@Component({providers : [EzAlgo] //声明依赖})class EzApp{//Angular2框架负责注入对象constructor(@Inject(EzAlgo) algo){//已经获得EzAlgo实例了!}}
在大多数情况下,我们只要在类的构造函数参数列表中使用Inject注解,就 可以告诉Angular2框架向我们的类代码中注入正确的对象。
那么,它是如何做到的?
Angular2的依赖注入机制实现的核心是一个作为第三方的注入器/Injector。通常使用Injector类的静态方法resolveAndCreate()来实例化一个注入器, 在实例化时需要指定所有的依赖项:
class A{...}class B{...}class C{...}var injector = Injector.resolveAndCreate([A,B,C]);
一旦获得了注入器实例,就可以使用其get()方法来获得指定的对象:
var a = injector.get(A); //A的实例var b = injector.get(B); //B的实例
值得指出的是,对于一个注入器而言,其仅仅维护每个依赖项的单一实例,也就是 说,无论你调用injector.get(A)多少遍,它总是返回A的同一个实例对象。
每当Angular2框架引导启动一个组件时,会自动根据组件Component注解的providers属性创建注入器,然后根据组件类的Inject注解,调用注入器的get()获取对应的 实例注入到组件类的构造函数中:

我们在前一节中,为了便于理解,有意含糊了一点。
下面的示例代码中,resolveAndCreate()的依赖项参数,其各个成员是什么类型?
var injector = Injector.cresolveAndCreate([A,B,C]);
看起来也许是:A代表class A,B代表class B,C代表class C。
也对,也不对。
事实上,依赖项是一个Provider对象,上面示例展开形式是:
var injector = Injector.cresolveAndCreate([new Provider(A,{useClass:A}),new Provider(B,{useClass:B}),new Provider(C,{useClass:C})]);
在Angular2中,注入器的依赖项总是一个Provider对象,当使用类作为提供者时,可以直接简写为类的名称,框架会自动转化为Provider对象。
Provider构造函数的第一个参数被称为TOKEN,可以理解为在注入器中,这个提供者的唯一标识,当我们使用Inject注解进行注入声明时,使用的就是这个TOKEN。
因此,Inject(A)中的A也不是指class A,而是一个TOKEN而已。我们可以换个字符串做TOKEN:
var injector = Injector.cresolveAndCreate([new Provider("AAA",{useClass:A}),new Provider("BBB",{useClass:B}),new Provider("CCCC",{useClass:C})]);
现在,要获得A类的实例就应当使用get("AAA")了,而注入A类的实例,就变成这样了:
class XYZ{constructor(@Inject("AAA") a){...}}
如果你不喜欢使用new,也可以使用provide()函数,它的参数与Provider类的构造 函数一样,并且直接返回一个Provider对象:
var injector = Injector.cresolveAndCreate([provide("AAA",{useClass:A}),provide("BBB",{useClass:B}),provide("CCCC",{useClass:C})]);
引入TOKEN的意义在于提供了第二个级别的解耦:你的代码将仅仅依赖于一个抽象的TOKEN,而非特定的类。
在下面的示例中,Render类定义了一个渲染器的接口,而DomRender类则是其接口的DOM实现版本, CanvasRender类是其接口的Canvas实现版本,我们使用Render作为TOKEN:

当我们需要使用DOM版本的Render时,只需要将TOKEN绑定到DomRender类:
var injector = Injector.resolveAndCreate([provide(Render,{useClass:DomRender})]);
当一段依赖于Render的代码运行时,被注入的就是DomRender实例:
class SomeCode{constructor(@Inject(Render) render){//got render -> DomRender}}
很显然,如果在创建注入器时,将TOKEN绑定到CanvasRender,上面的代码将被注入CanvasRender实例,而这一切,对于SomeCode都是透明的,也就是说,SomeCode仅仅依赖于一个抽象的TOKEN —— Render,它实现了与Render具体实现的解耦。
引入TOKEN的另一个目的在于,Provider不一定需要是一个类。
比如,你可以把应用的配置信息登记到注入器里,这样其他代码就可以在需要时直接注入了。 配置信息通常不是一个类,而是一个具体的JSON对象,我们可以在创建Privider对象 时,使用useValue来声明一个值提供者:
var cfg_provider = provide("APP_CONFIG",{useValue : {title:'My Site'}});var injector = Injector.resolveAndCreate([cfg_provider]);//使用var cfg = injector.get("APP_CONFIG");
工厂提供者是工厂模式的实现。在创建Provider对象时,使用useFactory属性指定一个 工厂函数,它应当返回一个具体的对象。
var car_provider = provide("CAR",{useFactory : function(){return new Car();}});var injector = Injector.resolveAndCreate([car_provider]);//使用var car = injector.get("CAR");
一个Provider可能依赖于其他的Provider,因此,在创建Provider对象时,有时需要使用deps属性来声明这一点,以便注入器能够首先创建这些依赖项,再实例化这个Provider:
class A{}class B{}class ABAB{constructor(@Inject(A) a ,@Inject(B) b){...}}var injector = Injector.resolveAndCreate([provide(A,{useClass:A}),provide(B,{useClass:B}),provide(C,{useClass:C,deps:[A,B]}]);
Angular2框架并不仅仅构造单一的注入器实例。事实上,当Angular2框架启动时, 将构造一个层级分别的注入器树:

bootstap(EzApp)的执行逻辑是这样的:
//创建PlatformRef对象var pf = platform(BROWSER_PROVIDERS);//创建ApplicationRef对象var app = pf.application([BROWSER_APP_PROVIDERS]);//启动根组件app.bootstrap(EzApp);
PlatformRef实例化根注入器,ApplicationRef对象的注入器原型指向它,而EzApp组件的注入器原型则指向ApplicationRef对象的注入器...
因此,当在EzB的构造函数中试图注入某个TOKEN时,Angular2会首先在当前的注入器中查找,如果找不到,则向上继续查找,直到根注入器。
考虑到依赖注入总是维护单例,因此如果需要在不同的组件中注入不同的实例,就应当在 不同的组件中分别使用Component注解的providers属性声明依赖项,而不是在父级声明依赖项。
一个Web应用通常需要切割为多个不同的组件进行实现,然后根据用户的交互行为(通常 是点击),动态地载入不同的组件,根据用户行为选择组件的过程就是路由:

由于Angular2是面向组件的,所以,Angular2的路由其实就是组件之间的路由。
在Angular2中使用路由的功能,有三个步骤:
为组件注入Router对象并通过config()方法配置路由:
router.config([{path:"/video", component:EzVideo},{path:"/music", component:EzMusic}])
上面的代码中,配置了两条路由:
如果用户请求路径为/video,那么将在路由出口中激活组件EzVideo
如果用户请求路径为/music,那么将在路由出口中激活组件EzMusic
路由出口是组件激活的地方,使用RouterOutlet指令在组件模板中声明出口:
@Component({directives:[ROUTER_DIRECTIVES],template : `<router-outlet></router-outlet>`})class EzApp{...}
使用Router的navigate()方法可以执行指定路径的路由,在示例中,当用户点击 时我们调用这个方法进行路由:
@View({template : `<span (click)="router.navigate('/video')">video</span> |<span (click)="router.navigate('/music')">music</span><router-outlet></router-outlet>`})
我们向navigate()方法传入的路径,就是我们通过config()方法配置的路径。这样,Router就根据这个路径,找到匹配的组件,在RouterOutlet上进行激活。
在真正开始使用路由的功能之前,我们需要做一些准备工作:
Angular2的路由库名为angular2/router,我们从这里引入常用类型:
import {LocationStrategy,Router,ROUTER_PROVIDERS,ROUTER_DIRECTIVES} from "angular2/router";
在启动组件时,我们需要声明路由相关的依赖类型(即变量:ROUTER_PROVIDERS),以便 根注入器可以解析对这些类型的请求:
bootstrap(EzApp,[ROUTER_PROVIDERS]);
除了使用Router的config()方法进行路由配置,Angular2还提供了路由注解,允许我们 以注解的方式为组件添加路由:
@RouteConfig([{path:"/video", component:EzVideo},{path:"/music", component:EzMusic}])class EzApp{...}
RouteConfigAnnotation的构造函数参数与Router的config()方法参数一致,都是一个包含 若干配置项的数组。
事实上,它的确只是一个语法糖 : Angular2在bootstrap一个组件时,会检查组件是否存在 RouteConfig注解,如果存在,则利用这个信息调用Router的config()方法进行路由配置:

不过这样写起来,感觉会流畅一些,声明式的氛围更强烈一些。
除了使用Router的navigate()方法切换路由,Angular2还提供了一个指令用来将一个DOM对象增强为路由入口:
@Component({directives:[ROUTER_PROVIDERS]template : `<nav><b [routerLink]="['Video']">video</b> |<b [routerLink]="['Music']">music</b></nav><router-outlet></router-outlet>`})
RouterLink指令为宿主DOM对象添加click事件监听,在触发时调用Router的navigate()方法进行路由。
需要指出的是,RouterLink并不能直接使用路由项的路径,routerLink属性的值是一个路由项的名称,我们需要在路由配置时通过name属性进行设定,必须遵循Pascal约定(首字母大写):
@RouteConfig([{path:"/video", component:EzVideo , name:"Video"},{path:"/music", component:EzMusic , name:"Music"}])
Router通过config()进行的路由配置,保存在路由注册表中;而Router通过navigate()执行路由时,利用的就是路由注册表中的信息:

在路由注册表中,保存有三种类型的路由表:
匹配表是最基本的路由决策表,它使用正则表达式实现路径向组件的匹配。
@RouteConfig([{path:"/video", component:EzVideo},{path:"/music", component:EzMusic}])
对应生成的匹配表以正则表达式为键值,映射到汇算后的路由项:
/^\/video$/g => {path:"/video", ...}/^\/music$/g => {path:"/music", ...}
如果在配置时指定了路径重定向,那么将在重定向表中生成路由项。
@RouteConfig([{path:"/video", component:EzVideo},{path:"/music", component:EzMusic},{path:"/test", redirectTo:"/video"}])
对应生成的重定向表以原始路径为键,映射到目标路径:
/test => /video
如果在配置路由时,通过name属性为路由项设置了名称,那么将为每个名称建立一个 路由项。我们已经知道,指令RouterLink需要使用名称表。
@RouteConfig([{path:"/video", component:EzVideo,name:"Video"},{path:"/music", component:EzMusic,name:"Music"}])
对应生成的名称表以名称为键值,映射到汇算后的路由项:
video => {path:"/video", ...},music => {path:"/music", ...}
有时我们希望不同的URL能够路由到同一个组件,比如:
/music/album/111/music/album/222
都路由到组件EzAlbum。
在RouteConfig注解中声明路由项的URL时,可以使用:varname来标记一个路由参数:
@RouteConfig([path:"/music/album/:aid",component:EzAlbum,name:"Music"])class EzApp{...}
这满足了将/music/album/111和/music/album/222都路由到EzAlbum组件的需求。
现在需要在EzAlbum组件中提取这个参数。
Angular2框架将路由参数封装为一个RouteParams对象,因此,我们在EzAlbum类的构造函数进行注入即可:
class EzAlbum{constructor(@Inject(RouteParams) params){this.aid = params.aid;//do sth.}}