@kungfuboy
2016-12-19T02:07:02.000000Z
字数 22225
阅读 1996
小源
在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.ts
export 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.
}
}