[关闭]
@kungfuboy 2016-12-19T02:07:02.000000Z 字数 22225 阅读 1996

Angular 2教程

小源


从搭建到运行

在Angular1.X版本的年代,我们通过script标签引入一个Angular脚本就能工作,但在Angular2不能这么干,它是面向未来的框架,有点超前,要求浏览器支持ES6+,我们现在要尝试的话,需要加一些垫片来抹平当前浏览器与ES6的差异:

不过,如果我们将上述几个文件打包成一个.js文件,就能像Angular1.X一样通过脚本引入工作了。

先写个Hello World

  1. import {Component} from "angular2/core";
  2. import {bootstrap} from "angular2/platform/browser";
  3. // 我们从angular2模块库中引入了两个类型: Component类和bootstrap函数。
  4. // 别问我怎么知道这两个类的位置,我也不知道!
  5. @Component({
  6. selector:"my-app",
  7. template:"<h1>Hello,World</h1>"
  8. })
  9. class myApp{}
  10. bootstrap(EzApp);

我们来逐行分析:

  1. @Component告诉Angular2:你将要解析一个组件。
  2. selector属性,告诉Angular2框架,这个组件渲染到哪个DOM对象上。
  3. template属性,告诉Angular2框架,使用什么模板进行渲染。
  4. bootstrap函数的作用是通知Angular2:请将组件渲染到DOM上!

@Component——注解

@Component给类myApp附加的元信息,也被称为注解或者Annotation。这个东西,就功能来看,有点像是其他语言中的装饰符,但是ES6并没有装饰符的概念,这实际上是转码器的一个特性:注解。

给一个类加注解,等同于设置这个类的annotations属性:

  1. // 注解写法
  2. @Component({selector:"my-app"})
  3. class myApp{...}
  4. // 等效于下面的写法
  5. class myApp{...}
  6. myApp.annotations = [new Component({selector:"my-app"})];
  7. // 注解在编译转码时,仅仅被放在类对象的annotation属性里,编译器并不进 行解释展开 —— 这个解释的工作是Angular2框架完成的

这通常不是编译器的功能,是Angular2团队特别要求的,因此我们可以看到,配置文件systemjs在使用TypeScript转码时默认是打开注解的:

  1. System.config({
  2. transpiler: 'typescript',
  3. typescriptOptions: { emitDecoratorMetadata: true },
  4. });

selector——声明选择符

使用Component注解的selector属性来告诉Angular2,当编译、链接模板时,如果 看到这个选择符,就实例化一个组件对象。

selector属性使用CSS选择符的匹配规则。

  1. // 匹配<my-app>...</my-app>
  2. @Component({selector:"ez-one",template:"TAGNAME-SELECTOR"})
  3. class EzOne{}
  4. // 匹配<any class="ez-two">...</any>
  5. @Component({selector:".ez-two",template:"CSSCLASS-SELECTOR"})
  6. class EzTwo{}
  7. // 匹配<any ez-three>...</any>
  8. @Component({selector:"[ez-three]",template:"ATTR-SELECTOR"})
  9. class EzThree{}
  10. // 匹配<any ez-four='123'>...</any>
  11. @Component({selector:"[ez-four=123]",template:"ATTRVAL-SELECTOR"})
  12. class EzFour{}

template/templateUrl——声明模板

内联模板

在ES6中,使用一对``符号就可以定义多行字符串,这比使用双引号要轻松不少。

  1. @Component({
  2. template : `<h1>hello</h1>
  3. <div>...</div>`
  4. })

外部模板

使用templateUrl属性可以引入外部模板,这可以让组件看起来更清爽一些。

  1. @Component({
  2. templateUrl : "ezcomp-tpl.html"
  3. })

styles/styleUrls - 设置样式

内联样式

  1. @Component({
  2. styles:[`
  3. h1{background:#4dba6c;color:#fff}
  4. `]
  5. })

外部样式

显然我们会更倾向于这种,不然sass上哪去写去?

  1. @Component({
  2. styleUrls:["ez-greeting.css"]
  3. })

properties - 声明属性

属性是组件暴露给外部世界的调用接口,调用者通过设置不同的属性值来定制组件的行为与外观。在Angular2中为组件增加属性接口非常简单,只需要在Componentproperties属性中声明组件的成员变量就可以了:

  1. //myCard
  2. @Component({
  3. properties:["name","country"]
  4. })
  5. ···
  6. // myApp
  7. @Component({
  8. directives : [myCard],
  9. template : `<ez-card [name]="'雷锋'" [country]="'中国'"></ez-card>`
  10. })

events - 声明事件

与属性相反,事件从组件的内部流出,用来通知外部世界发生了一些事情。在Angular2中为组件增加事件接口也非常简单:定义一个事件源/EventEmitter,然后通过Componentevents接口暴露出来:

  1. @Component({
  2. events:["change"]
  3. })
  4. class myCard{
  5. constructor(){
  6. this.change = new EventEmitter();
  7. }
  8. }

上面的代码将组件myCard的事件源change暴露为同名事件,这意味着在调用者 myApp组件的模板中,可以直接使用小括号语法挂接事件监听函数:

  1. // myApp
  2. @Component({
  3. template : "<ez-card (change)="onChange()"></ez-card>"
  4. })

directives - 引用指令

组件,就是指令

{{model}} - 文本插值

在模板中使用可以{{表达式}}的方式绑定组件模型中的表达式,当表达式变化时,Angular2将自动更新对应的DOM对象:

  1. @Component({
  2. selector:"ez-app",
  3. template: `<div>
  4. <h1>{{title}}</h1>
  5. <p>{{content}}</p>
  6. <div>
  7. <span>{{date}}</span> 来源:<span>{{source}}</span>
  8. </div>
  9. </div>`
  10. })

[property] - 属性绑定

在模板中,也可以使用一对中括号将HTML元素或组件的属性绑定到组件模型的某个表达式,当表达式的值变化时,对应的DOM对象将自动得到更新。

  1. @Component({
  2. selector:"my-app",
  3. template:`<h1 [style.color]="color">Hello,Angular2</h1>`
  4. })
  5. // 等价于
  6. @Component({
  7. selector:"ez-app",
  8. template:`<h1 bind-style.color="color">Hello,Angular2</h1>`
  9. })

除了用中括号绑定之外,也可以用bind-前缀来绑定。

需要注意的是,属性的值总是被当做调用者模型中的表达式进行绑定,当表达式变化时,被调用的组件自动得到更新。如果希望将属性绑定到一个常量字符串,别忘了给字符串加引号,或者去掉中括号:

  1. //错误,Angular2将找不到表达式 Hello,Angular2
  2. @Component({template:`<h1 [textContent]="Hello,Angular2"></h1>`})
  3. //正确,Angular2识别出常量字符串表达式 'Hello,Angular2'
  4. @Component({template:`<h1 [textContent]="'Hello,Angular2'"></h1>`})
  5. //正确,Angular2识别出常量字符串作为属性textContent的值
  6. @Component({template:`<h1 textContent="Hello,Angular2"></h1>`})

(event) - 事件绑定

在模板中为元素添加事件监听很简单,使用一对小括号包裹事件名称,并绑定到表达式即可。

  1. @View({template : `<h1 (click)="roulette()">ROULETTE</h1>`})
  2. // 等价于
  3. @View({template : `<h1 on-click="roulette()">ROULETTE</h1>`})

#var - 局部变量

有时模板中的不同元素间可能需要互相调用,Angular2提供一种简单的语法将元素映射为局部变量:添加一个以#var-开始的属性,后续的部分表示变量名, 这个变量对应元素的实例。

在下面的代码示例中,我们为元素h1定义了一个局部变量v_h1,这个变量指向该元素对应的DOM对象,你可以在模板中的其他地方调用其方法和属性:

  1. @Component({
  2. template : `
  3. <h1 #v_h1>hello</h1>
  4. <button (click) = "v_h1.textContent = 'HELLO'">test</button>
  5. `
  6. })

如果在一个组件元素上定义局部变量,那么其对应的对象为组件的实例:

  1. @Component({
  2. directives:[EzCalc],
  3. template : "<ez-calc #c></ez-calc>"
  4. })

在上面的示例中,模板内的局部变量c指向EzCalc的实例。

NgStyle - 内联样式

我们可以使用样式绑定的方法设置单一样式。但如果要同时设置多个样式值,可以使用 NgStyle指令,将内联样式绑定到组件的属性上。

NgStyle指令的选择符是[ngStyle],下面的示例将div元素的样式绑定到组件的 styles属性:

  1. <div [ngStyle]="styles">...</div>

styles属性则当然是在constructor()里定义:

  1. this.styles = {
  2. "color" : "red",
  3. "font-style" : "italic",
  4. "background-color" : "black"
  5. }
  6. //等价于
  7. this.styles = {
  8. color: "red"
  9. fontStyle : "italic",
  10. backgroundColor : "black"
  11. }

NgClass - 样式类

在Web App中,我们通常采用动态添加或删除样式类的方法,来改变DOM元素的外观表现。

当然,我们可以HTML元素的class属性绑定到组件实例的属性上,然后通过改变实例的属性, 实现动态修改HTML元素的样式类:

  1. <div [class]="cls">...</div>

不过,如果一次要删除或添加多个.class,Angular2内置的NgClass指令会更简单。NgClass指令的选择符是ngClass,下面的示例将div元素的class属性绑定到组件的cns属性:

  1. <div [ngClass]="cns">...</div>

cns属性是一个JSON对象,每个键代表样式类名,对应的值为true时表示向HTML元素 添加该样式类,为false时表示删除该样式类。如果cns的值如下:

  1. this.cns = {
  2. colorful:true,
  3. italic : false,
  4. bold : true
  5. }

如此,该模板渲染之后的结果是:

  1. <div class="colorful bold">...</div>

使用条件逻辑

有时我们需要模板的一部分内容在满足一定条件时才显示,这是指令NgIf发挥作用的场景,它评估属性ngIf的值是否为真,来决定是否渲染template元素的内容:

  1. @Component({
  2. template : `
  3. <!--根据变量trial的值决定是否显示图片-->
  4. <template [ngIf]="trial==true">
  5. <img src="ad.jpg">
  6. </template>
  7. <!--以下是其他必须显示的元素-->
  8. <pre>...
  9. ` })

Angular2同时提供了两种语法糖,让NgIf写起来更简单,下面的两种书写方法和上面的正式语法是等效的:

  1. //使用template attribute
  2. <img src="ad.jpg" template="ngIf tiral==true">
  3. //使用*前缀
  4. <img src="ad.jpg" *ngIf="tiral==true">

看起来,显然*ngIf的书写方法更亲切一些,不过无论采用哪种书写方法,都将转换成上面的正式写法,再进行编译。

使用分支逻辑

如果组件的模板需要根据某个表达式的不同取值展示不同的片段,可以使用NgSwitch系列指令来动态切分模板。NgSwitch包含一组指令,用来构造包含多分支的模板:

NgSwitch指令可以应用在任何HTML元素上,它评估元素的ngSwitch属性值,并根据这个值决定应用哪些template的内容(可以同时显示多个分支):

  1. <ANY [ngSwitch]="expression">...</ANY>

NgSwitchWhen指令必须应用在NgSwitch指令的子template元素上,它通过属性ngSwitchWhen指定一个表达式, 如果该表达式与父节点的NgSwitch指令指定的表达式值一致,那么显示这个template的内容:

  1. <ANY [ngSwitch]="...">
  2. <!--与变量比较-->
  3. <template [ngSwitchWhen]="variable">...</template>
  4. <!--与常量比较-->
  5. <template ngSwitchWhen="constant">...</template>
  6. </ANY>

NgSwitchDefault指令必须应用在NgSwitch指令的子template元素上,当没有NgSwitchWhen指令匹配 时,NgSwitch将显示这个template的内容:

  1. <ANY [ngSwitch]="...">
  2. <template ngSwitchDefault>...</template>
  3. </ANY>

也就是说,整个组合的逻辑是这样的:

  1. @Component({
  2. selector : "myApp",
  3. properties:["gender"],
  4. template : `
  5. <div [ngSwitch]="gender"> //判断开始
  6. <template ngSwitchWhen="Male"> //有匹配执行
  7. <img src="img/male-ad.jpg" class="banner">
  8. </template>
  9. <template ngSwitchWhen="Female"> //有匹配执行
  10. <img src="img/female-ad.png" class="banner">
  11. </template>
  12. <template ngSwitchDefault> //默认或无匹配执行
  13. <h1>Learn Something, NOW!</h1>
  14. </template>
  15. </div>`
  16. })

NgFor- 循环逻辑

如果希望利用一组可遍历的数据动态构造模板,那么应当使用NgFor指令。

迭代

NgFor指令应用在template元素上,对ngForOf属性指定的数据集中的每一项实例化一个template的内容:

  1. <template ngFor [ngForOf]="items" >
  2. <li>----------</li>
  3. </template>

如果items数据集有3条记录,那么会生成3个li对象,就像这样:

  1. <li>----------</li>
  2. <li>----------</li>
  3. <li>----------</li>

不过这没多大用。

使用数据项

好在我们还可以为数据集的每一项声明一个局部变量,以便在模板内引用:

  1. <template ngFor [ngForOf]="items" #item>
  2. <li>{{item}}</li>
  3. </template>

假如items数据集是一个数组:["China","India","Russia"],那么现在生成的结果就有点用了:

  1. <li>China</li>
  2. <li>India</li>
  3. <li>Russia</li>

使用数据项索引

有时还需要数据项在数据集中的索引,我们也可以为数据集的每一项的索引声明一个局部变量,以便在模板内引用:

  1. <template ngFor [ngForOf]="items" #item #i="index">
  2. <li>[{{i+1}}] {{item}}</li>
  3. </template>

现在生成的结果更规矩了:

  1. <li>[1] China</li>
  2. <li>[2] India</li>
  3. <li>[3] Russia</li>

语法糖

NgIf类似,Angular2也为NgFor提供了两种语法糖:

  1. //使用template attribute
  2. <ANY template="ngFor #item of items;#i=index">...</ANY>
  3. //使用*前缀
  4. <ANY *ngFor="#item of items;#i=index">...</ANY>

毫无疑问,应当尽量使用*ngFor的简便写法,这可以提高模板的可读性。

管道

有时候需要在表现层对数据进行处理,Angular2的管道就派上用场了,管道的作用的是:在模板中对输入数据进行变换,并输出变换后的结果。在模板中,使用|来调用一个管道操作,使用:来向管道传入参数。

例如:{{ data | <pipename>:<arg1>:<arg2> }}

Angular2中已经有了许多预置的管道来对常见数据类型进行处理:

DatePipe - 对日期/时间数据进行格式变换。

  1. {{ day | date:'yyMMdd'}}

JsonPipe - 将JSON对象转换为字符串,其实现基于JSON.stringify()

  1. {{ {x:1,y:2} | json }}

PercentPipe - 将数值转换为百分比。

  1. {{ 1.23456 | percent:'1.2-3' }}

SlicePipe - 提取输入字符串中的指定切片

在模板中使用slice来引用SlicePipe。第一个参数指定切片的起始索引, 第二个参数指定切片的终止索引的下一个。

  1. <!--结果:'123'-->
  2. {{ '01234567890' | slice:1:4 }}

UpperCasePipe - 将输入字符串变换为大写

在模板中使用uppercase来引用UpperCasePipe

  1. <!--结果:THIS IS A DEMO-->
  2. {{ "this is a demo" | uppercase }}

LowerCasePipe - 将输入字符串变换为小写

在模板中使用lowercase来引用LowerCasePipe

  1. <!--结果:what a wonderful world-->
  2. {{ "WHAT A WONDERFUL WORLD" | lowercase }}

管道可以联级

定制管道

声明元数据

和实现一个组件类似,一个管道也是具有特定元数据的类:

  1. @Pipe({name:'ezp'})
  2. class EzPipe{...}

Pipe注解为被装饰的类附加了管道元数据,其最重要的属性是name,也就是 我们在模板中调用这个管道时使用的名称。上面定义的管道,我们可以在模板中 这样使用:{{ data | ezp }}

实现transform方法

管道必须实现一个预定的方法transform(input,args),这个方法的input参数代表输入数据,args参数代表输入参数,返回值将被作为管道的输出。

下面的示例简单地将输入数据与所有参数拼接在一起:

  1. @Pipe({name:'ezp'})
  2. class EzPipe{
  3. transform(input,args){
  4. return input + " " +args.join(" ");
  5. }
  6. }

使用自定义管道

在组件的模板中使用自定义管道之前,需要预先声明一下,以便Angular2注入。 使用Component注解的pipes属性进行声明:pipes:[EzPipe]

现在,我们就可以使用这个自定义管道了:

  1. <!--结果:"call join mary linda"-->
  2. {{ "call " | ezp:'john':'mary':'linda' }}

有状态管道

我们之前了解的管道,包括Angular2预置的管道,以及我们自己实现的管道,都有一个特点, 就是输出仅仅依赖于输入,这样的管道,在Angular2中被称为无状态管道/Stateless Pipe

对于无状态管道,当输入没有变化时,Angular2框架不会重新计算管道的输出。但也许有些时候,我们希望即使输入没有变化,也持续地检测管道的输出。例如,我们设计了一个倒计时管道,向 它输入一个秒数,会自动多次输出直至0秒:

  1. {{ 10 | countdown }}

实现countdown的逻辑很简单,记录起始值,然后开一个1秒1次的计时器,逐次减至0秒即可。

关键在于,默认情况下,Angular2框架仅仅执行一次管道的transform()方法,我们需要 使用Pipe注解的pure属性值为false,要求Angular2框架在每个变化检查周期都执行 管道的transform()方法:

  1. @pipe({
  2. name:"countdown",
  3. pure : false
  4. })
  5. class EzCountdown{...}

很显然,countdown管道的输出不仅依赖于输入,还依赖于其内部的运行状态。因此,这样 的管道,在Angular2中被称为有状态管道/Stateful Pipe

注意

需要指出的是,管道的有状态与无状态的区别,关键在于是否需要Angular2框架在输入不变的 情况下依然持续地进行变化检测,而不在于我们通常所指的计算的幂等性 - 即同样的输入 总是产生同样的输出。

例如,一个计算累加值的管道,在传统的概念中,应当被视为有状态的,因为它对于同样的输 入,会累加之前记录的总和,因此会产生不同的输出。但是,在Angular2中,它依然被视为无状态的,因为,它的一次输入不会产生多次输出。

AsyncPipe

AsyncPipe是Angular2框架预置的一个有状态管道,它的输入是一个异步对象:Promise对象、Observable对象或者EventEmitter对象。

每当异步对象产生新的值,AsyncPipe会返回这个新的值,因此,AsyncPipe需要Angular2框架持续进行变化检测,它的Pipe注解的pure属性值为false

我们可以使用AsyncPipe来重写之前的EzCountdown管道,让它返回一个Observable

  1. @Pipe({name : "countdown"})
  2. class EzCountdown{
  3. transform(input){
  4. var counter = input;
  5. return new Observable(o => {
  6. setInterval(_ => {
  7. o.next(counter);
  8. counter--;
  9. if(counter<0) o.complete();
  10. },1000);
  11. });
  12. }
  13. }

你注意到现在EzCountdown是一个无状态的管道了,它返回一个Observable对象,我们在模板中使用AsyncPipe来继续处理这个对象:

  1. {{ 10 | countdown | async }}

简单地说,ObservableEventEmitter适合用多次返回结果的异步应用场景,而Promise则适合于仅返回一次结果的异步应用场景。

NgForm - 表单指令

NgForm指令为表单元素/form建立一个控件组对象,作为控件的容器。而NgControlName指令为则为宿主input元素建立一个控件对象,并将该控件加入到NgForm指令建立的控件组中:

image_1b499cvlnouu1mqp1u141uht1fn713.png-80kB

通过使用#符号,我们创建了一个引用控件组对象(注意,不是form元素!)的局部变量f。 这个变量最大的作用是:它的value属性是一个简单的JSON对象,键对应于input元素的 ng-control属性,值对应于input元素的值:

声明指令依赖

NgForm指令和NgControlName指令都包含在预定义的数组变量FORM_DIRECTIVES中,所以我们在组件注解的directives属性中直接声明FORM_DIRECTIVES就可以在模板中直接使用这些指令了:

  1. //angular2/src/common/forms/directives.ts
  2. export const FORM_DIRECTIVES: Type[] = CONST_EXPR([
  3. NgControlName,
  4. NgControlGroup,
  5. NgFormControl,
  6. NgModel,
  7. NgFormModel,
  8. NgForm,
  9. NgSelectOption,
  10. DefaultValueAccessor,
  11. NumberValueAccessor,
  12. CheckboxControlValueAccessor,
  13. SelectControlValueAccessor,
  14. NgControlStatus,
  15. RequiredValidator,
  16. MinLengthValidator,
  17. MaxLengthValidator
  18. ]);

NgControlName - 命名控件指令

如前所述,NgControlName指令必须作为NgFormNgFormModel的后代使用,因为这个指令需要将创建的控件对象添加到祖先(NgFormNgFormModel)所创建的控件组中。

NgControlName指令的选择符是[ngControl],这意味着你必须在一个HTML元素上 定义ngControl属性,这个指令才会起作用。

属性:ngControl

NgControlName指令为宿主的DOM对象创建一个控件对象,并将这个对象以ngControl属性 指定的名称绑定到DOM对象上:

  1. <form #f="ngForm">
  2. <input type="text" ngControl="user">
  3. <input type="password" ngControl="pass">
  4. </form>

在上面的代码中,将创建两个Control对象,名称分别为userpass

属性/方法:ngModel

除了使用控件组获得输入值,NgControlName指令可以通过ngModel实现模型与表单的双向绑定:

  1. <form>
  2. <input type="text" ngControl="user" [(ngModel)]="data.user">
  3. <input type="password" ngControl="pass" [(ngModel)]="data.pass">
  4. </form>

ngModel即是NgControlName指令的属性,也是它的事件,所以下面的两种写法是等价的:

  1. <input type="text" ngControl="user" [(ngModel)]="data.user">
  2. //等价于
  3. <input type="text" ngControl="user" [ngModel]="data.user" (ngModel)="data.user">

NgControlGroup - 命名控件组

NgControlGroup指令的选择符是[ng-control-group],如果模板中的某个元素具有这个属性, Angular2框架将自动创建一个控件组对象,并将这个对象以指定的名称与DOM对象绑定。

控件组可以嵌套,方便我们在语义上区分不同性质的输入:

image_1b499vp491csb1aqg1t1i3oa1ev81g.png-46.6kB

NgControlName指令一样,NgControlGroup指令也必须作为NgFormNgFormModel的后代使用,因为这个指令需要将创建的控件组对象添加到祖先(NgFormNgFormModel)所创建 的控件组中。

NgFormControl - 绑定已有控件对象

NgControlName指令不同,NgFormControl将已有的控件/Control对象绑定到DOM元素上。当需要对输入的值进行初始化时,可以使用NgFormControl指令。

下面的代码中,使用NgFormControl指令将DOM元素绑定到组件EzComp的成员 变量movie上,我们需要在构造函数中先创建这个Control对象:

  1. @View({
  2. //将输入元素绑定到已经创建的控件对象上
  3. template : `<input type="text" [ngFormControl]="movie">`
  4. })
  5. class EzComp{
  6. constructor(){
  7. //创建控件对象
  8. this.movie = new Control("Matrix II - Reload");
  9. }
  10. }

控件/Control是Angular2中对表单输入元素的抽象,我们使用其value属性,就可以获得对应的输入元素的值。

NgControlName指令的另一个区别是,NgFormControl不需要NgFormNgFormModel的祖先。

NgFormModel - 绑定已有控件组

NgFormModel指令类似于NgControlGroup指令,都是为控件提供容器。但区别在于,NgFormModel指令将已有的控件组绑定到DOM对象上:

  1. @View({
  2. template : `
  3. <!--绑定控件组与控件对象-->
  4. <div [ngFormModel]="controls">
  5. <input type="text" ngControl="name">
  6. <input type="text" ngControl="age">
  7. </div>`
  8. })
  9. class EzComp{
  10. constructor(){
  11. //创建控件组及控件对象
  12. this.controls = new ControlGroup({
  13. name :new Control("Jason"),
  14. age : new Control("45")
  15. });
  16. }
  17. }

NgFormModel指令可以包含NgControlGroup指令,以便将不同性质的输入分组。

Directive - 定义指令元数据

指令是Angular对HTML进行扩展的基本手段。与Angular1.x不同,在Angular2中,指令被明确地划分为三种类型:

  • 组件 - 组件其实就是带有模板的指令
  • 属性指令 - 属性指令用来改变所在元素的外观或行为,例如NgClass和NgStyle指令
  • 结构指令 - 结构指令用来向DOM中添加或删除元素,例如NgIf和NgFor指令。

组件使用Component注解来装饰组件类,而属性指令和结构指令则使用Directive注解 来装饰指令类。

Directive注解 - 声明指令元数据

Directive注解最重要的属性是selector,它指定了触发Angular2框架生成指令实例 的CSS选择符。下面的示例定义了一个指令EzDirective

  1. @Directive({selector:"[ez-h]"})
  2. class EzHilight{...}

模板中具有ez-h的元素,Angular2框架都将为其生成一个EzDirective类实例。例如, 下面的模板中,框架姜维div元素实例化EzHilight:

  1. <div ez-h>...</div>

ElementRef - 获取指令所在DOM对象

很显然,我们需要在EzDirective类的实现中进行DOM操作,这需要告诉Angular2框架向我们 注入ElementRef对象,其nativeElement属性就是对应的DOM对象:

  1. class EzHilight{
  2. constructor(@Inejct(ElementRef) elref){
  3. this.el = elref.nativeElement; //获取指令所在的DOM元素
  4. this.el.styles.color = "red"; //进行DOM操作
  5. }
  6. }

使用自定义指令

如果要在组件中使用自定义指令,需要在Component注解中设置directives属性:

  1. @Component({
  2. selector : "ez-app",
  3. template : "<div ez-h>...</div>",
  4. directives : [EzHilight]
  5. })
  6. class EzApp{}

inputs - 声明属性值映射

很显然,在定义组件模板时,我们通常会给属性设定一个值,比如,我们希望以下的模板片段中,将指令所在的DOM对象的背景设置为指定颜色:

  1. <div [ez-h]="'black'">...</div>

通过使用Directive注解的inputs属性,我们可以将DOM对象的属性映射到指令对象的属性,例如,对于下面定义的指令:

  1. @Directive({
  2. selector : "[ez-h]",
  3. inputs : ["bgColor:ez-h"]
  4. })
  5. class EzHilight{...}

当在模板中使用这个指令时,EzHilight对象的bgColor属性自动绑定到模板中div元素的ez-h属性的值。对于指令而言,这是一个输入,每当ez-h发生变化,Angular2 将自动设置EzHilightbgColor属性。

我们可以使用ES6中的setter,在EzHilight中捕捉每个变化的时刻:

  1. class EzHilight{
  2. set bgColor(v){
  3. this.el.style.background = v;
  4. }
  5. }

修改示例代码,为EzHilight指令增加一个新的属性color

host - 声明事件监听

如果指令的实现需要监听所在DOM元素的事件,可以使用Directive注解的host属性。

下面的示例中,指令将监听所在DOM元素的两个事件 - clickmouseover

  1. @Directive({
  2. selector : "[ez-h]",
  3. host : {
  4. '(click)':'onMyClick()',
  5. '(mouseover)':'onMyMouseOver()'
  6. }
  7. })
  8. class EzHilight{...}

你看到,host属性的值应当是一个JSON对象,其键为一对小括号包裹的事件名称,书写方法与在模板中一致;值为事件处理表达式,通常是对指令类中方法的调用。例如:

  1. class EzHilight{
  2. onMyClick(){...}
  3. onMyMouseOver(){...}
  4. }

renderer - 使用渲染器

EzHilight指令的实现中,我们是直接通过ElementRef对象的nativeElement属性来直接操作浏览器DOM的,不过Angular2其实不希望我们这么做,因为这将使我们的代码与浏览器纠缠不清,有违Angular2的跨平台本意 —— 换句话说,这么直接操作DOM的做法,是反模式的。

在Angular2中,引入了渲染器/renderer的概念,它定义了一组规范的接口Renderer,对于不同的平台,有不同的实现。比如,对于浏览器,对应的Renderer实现是DomRenderer

在指令的构造函数中,我们可以要求Angular2框架注入当前使用的渲染器对象:

  1. class EzHilight{
  2. constructor(@Inject(ElementRef) el,@Inject(Renderer) renderer){
  3. this.el = el;
  4. this.renderer = renderer;
  5. }
  6. }

Angular2希望我们使用Renderer来代替直接的DOM操作,这将保证我们的代码获得跨平台特性。现在我们使用RenderersetElementStyle()方法来修改样式:

  1. class EzHilight{
  2. set bgColor(v){
  3. this.renderer.setElementStyle(this.el,"background",v);
  4. }
  5. }

服务 - 封装可复用代码

在Angular2中,服务用来封装可复用的功能性代码。比如Http服务,封装了ajax请求的细节,在不同的组件中,我们只需要调用Http服务的API接口就可以给组件增加 ajax请求的功能了:

image_1b49an70p68213ha1nqqhm1t561t.png-16.6kB

Angular2中实现一个服务非常简单直接 : 定义一个类,然后,它就是服务了:

  1. class EzAlgo{
  2. add(a,b){return a+b;}
  3. sub(a,b){return a-b;}
  4. }

上面的代码定义了一个相当弱智的算法服务EzAlgo,它有两个API - add()用来 计算两个数的和,sub()用来计算两个数的差 。在示例中,组件EzApp依赖于这个 服务:

image_1b49anl6e15vu2l1suj1nhhq4g2a.png-7kB

注入服务 - providers

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

image_1b49aom6vn92nbg1ntutu61m512n.png-30.6kB

注入器就像婚姻介绍所,男方在婚介所登记心仪的女性特点,约好见面地点,然后, 坐等发货即可。比如上图:

EzApp组件(男方)使用Component注解的appInjector属性向Angular2框架(婚介所)声明其依赖于EzAlgo(登记心仪的女性特点),并在其构造函数的参数表中使用Inject注解声明注入点(约好见面地点),然后,剩下的事儿Angular2(婚介所)就办了:

  1. @Component({
  2. providers : [EzAlgo] //声明依赖
  3. })
  4. class EzApp{
  5. //Angular2框架负责注入对象
  6. constructor(@Inject(EzAlgo) algo){
  7. //已经获得EzAlgo实例了!
  8. }
  9. }

Injector - 注入器

在大多数情况下,我们只要在类的构造函数参数列表中使用Inject注解,就 可以告诉Angular2框架向我们的类代码中注入正确的对象。

那么,它是如何做到的?

Angular2的依赖注入机制实现的核心是一个作为第三方的注入器/Injector。通常使用Injector类的静态方法resolveAndCreate()来实例化一个注入器, 在实例化时需要指定所有的依赖项:

  1. class A{...}
  2. class B{...}
  3. class C{...}
  4. var injector = Injector.resolveAndCreate([A,B,C]);

一旦获得了注入器实例,就可以使用其get()方法来获得指定的对象:

  1. var a = injector.get(A); //A的实例
  2. var b = injector.get(B); //B的实例

值得指出的是,对于一个注入器而言,其仅仅维护每个依赖项的单一实例,也就是 说,无论你调用injector.get(A)多少遍,它总是返回A的同一个实例对象。

每当Angular2框架引导启动一个组件时,会自动根据组件Component注解的providers属性创建注入器,然后根据组件类的Inject注解,调用注入器的get()获取对应的 实例注入到组件类的构造函数中:

image_1b49apvhmhmjiek2o1mpcdvf34.png-51.6kB

Provider - 服务提供者

我们在前一节中,为了便于理解,有意含糊了一点。

下面的示例代码中,resolveAndCreate()的依赖项参数,其各个成员是什么类型?

  1. var injector = Injector.cresolveAndCreate([A,B,C]);

看起来也许是:A代表class A,B代表class B,C代表class C。

也对,也不对。

事实上,依赖项是一个Provider对象,上面示例展开形式是:

  1. var injector = Injector.cresolveAndCreate([
  2. new Provider(A,{useClass:A}),
  3. new Provider(B,{useClass:B}),
  4. new Provider(C,{useClass:C})
  5. ]);

在Angular2中,注入器的依赖项总是一个Provider对象,当使用类作为提供者时,可以直接简写为类的名称,框架会自动转化为Provider对象。

Provider构造函数的第一个参数被称为TOKEN,可以理解为在注入器中,这个提供者的唯一标识,当我们使用Inject注解进行注入声明时,使用的就是这个TOKEN

因此,Inject(A)中的A也不是指class A,而是一个TOKEN而已。我们可以换个字符串做TOKEN

  1. var injector = Injector.cresolveAndCreate([
  2. new Provider("AAA",{useClass:A}),
  3. new Provider("BBB",{useClass:B}),
  4. new Provider("CCCC",{useClass:C})
  5. ]);

现在,要获得A类的实例就应当使用get("AAA")了,而注入A类的实例,就变成这样了:

  1. class XYZ{
  2. constructor(@Inject("AAA") a){...}
  3. }

如果你不喜欢使用new,也可以使用provide()函数,它的参数与Provider类的构造 函数一样,并且直接返回一个Provider对象:

  1. var injector = Injector.cresolveAndCreate([
  2. provide("AAA",{useClass:A}),
  3. provide("BBB",{useClass:B}),
  4. provide("CCCC",{useClass:C})
  5. ]);

TOKEN - 服务标识

引入TOKEN的意义在于提供了第二个级别的解耦:你的代码将仅仅依赖于一个抽象的TOKEN,而非特定的类。

在下面的示例中,Render类定义了一个渲染器的接口,而DomRender类则是其接口的DOM实现版本, CanvasRender类是其接口的Canvas实现版本,我们使用Render作为TOKEN

image_1b49b00voq2sjvq74g1ev0i493h.png-17.8kB

当我们需要使用DOM版本的Render时,只需要将TOKEN绑定到DomRender类:

  1. var injector = Injector.resolveAndCreate([
  2. provide(Render,{useClass:DomRender})
  3. ]);

当一段依赖于Render的代码运行时,被注入的就是DomRender实例:

  1. class SomeCode{
  2. constructor(@Inject(Render) render){
  3. //got render -> DomRender
  4. }
  5. }

很显然,如果在创建注入器时,将TOKEN绑定到CanvasRender,上面的代码将被注入CanvasRender实例,而这一切,对于SomeCode都是透明的,也就是说,SomeCode仅仅依赖于一个抽象的TOKEN —— Render,它实现了与Render具体实现的解耦。

Provider的其他选项

引入TOKEN的另一个目的在于,Provider不一定需要是一个类。

值提供者/Value Provider

比如,你可以把应用的配置信息登记到注入器里,这样其他代码就可以在需要时直接注入了。 配置信息通常不是一个类,而是一个具体的JSON对象,我们可以在创建Privider对象 时,使用useValue来声明一个值提供者:

  1. var cfg_provider = provide("APP_CONFIG",{
  2. useValue : {title:'My Site'}
  3. });
  4. var injector = Injector.resolveAndCreate([cfg_provider]);
  5. //使用
  6. var cfg = injector.get("APP_CONFIG");

工厂提供者/Factory Provider

工厂提供者是工厂模式的实现。在创建Provider对象时,使用useFactory属性指定一个 工厂函数,它应当返回一个具体的对象。

  1. var car_provider = provide("CAR",{
  2. useFactory : function(){return new Car();}
  3. });
  4. var injector = Injector.resolveAndCreate([car_provider]);
  5. //使用
  6. var car = injector.get("CAR");

声明依赖项

一个Provider可能依赖于其他的Provider,因此,在创建Provider对象时,有时需要使用deps属性来声明这一点,以便注入器能够首先创建这些依赖项,再实例化这个Provider

  1. class A{}
  2. class B{}
  3. class ABAB{
  4. constructor(@Inject(A) a ,@Inject(B) b){...}
  5. }
  6. var injector = Injector.resolveAndCreate([
  7. provide(A,{useClass:A}),
  8. provide(B,{useClass:B}),
  9. provide(C,{useClass:C,deps:[A,B]}
  10. ]);

Injector Tree - 注入器树

Angular2框架并不仅仅构造单一的注入器实例。事实上,当Angular2框架启动时, 将构造一个层级分别的注入器树:

image_1b49b5u4np4p9o31c7n17ms1od43u.png-12.7kB

bootstap(EzApp)的执行逻辑是这样的:

  1. //创建PlatformRef对象
  2. var pf = platform(BROWSER_PROVIDERS);
  3. //创建ApplicationRef对象
  4. var app = pf.application([BROWSER_APP_PROVIDERS]);
  5. //启动根组件
  6. app.bootstrap(EzApp);

PlatformRef实例化根注入器,ApplicationRef对象的注入器原型指向它,而EzApp组件的注入器原型则指向ApplicationRef对象的注入器...

因此,当在EzB的构造函数中试图注入某个TOKEN时,Angular2会首先在当前的注入器中查找,如果找不到,则向上继续查找,直到根注入器。

考虑到依赖注入总是维护单例,因此如果需要在不同的组件中注入不同的实例,就应当在 不同的组件中分别使用Component注解的providers属性声明依赖项,而不是在父级声明依赖项。

路由 - 初体验

一个Web应用通常需要切割为多个不同的组件进行实现,然后根据用户的交互行为(通常 是点击),动态地载入不同的组件,根据用户行为选择组件的过程就是路由:

image_1b49b98ntavs3kq1d5j1oarbpu4b.png-25.7kB

由于Angular2是面向组件的,所以,Angular2的路由其实就是组件之间的路由。

路由 - 应用步骤

在Angular2中使用路由的功能,有三个步骤:

  1. 配置路由

为组件注入Router对象并通过config()方法配置路由:

  1. router.config([
  2. {path:"/video", component:EzVideo},
  3. {path:"/music", component:EzMusic}])

上面的代码中,配置了两条路由:

如果用户请求路径为/video,那么将在路由出口中激活组件EzVideo
如果用户请求路径为/music,那么将在路由出口中激活组件EzMusic

  1. 设置路由出口

路由出口是组件激活的地方,使用RouterOutlet指令在组件模板中声明出口:

  1. @Component({
  2. directives:[ROUTER_DIRECTIVES],
  3. template : `<router-outlet></router-outlet>`
  4. })
  5. class EzApp{...}
  1. 执行路由

使用Router的navigate()方法可以执行指定路径的路由,在示例中,当用户点击 时我们调用这个方法进行路由:

  1. @View({
  2. template : `
  3. <span (click)="router.navigate('/video')">video</span> |
  4. <span (click)="router.navigate('/music')">music</span>
  5. <router-outlet></router-outlet>`
  6. })

我们向navigate()方法传入的路径,就是我们通过config()方法配置的路径。这样,Router就根据这个路径,找到匹配的组件,在RouterOutlet上进行激活。

在真正开始使用路由的功能之前,我们需要做一些准备工作:

  1. 引入路由相关的预定义类型

Angular2的路由库名为angular2/router,我们从这里引入常用类型:

  1. import {LocationStrategy,Router,ROUTER_PROVIDERS,ROUTER_DIRECTIVES} from "angular2/router";
  1. 声明路由相关依赖类型

在启动组件时,我们需要声明路由相关的依赖类型(即变量:ROUTER_PROVIDERS),以便 根注入器可以解析对这些类型的请求:

  1. bootstrap(EzApp,[ROUTER_PROVIDERS]);

RouteConfig - 路由配置注解

除了使用Routerconfig()方法进行路由配置,Angular2还提供了路由注解,允许我们 以注解的方式为组件添加路由:

  1. @RouteConfig([
  2. {path:"/video", component:EzVideo},
  3. {path:"/music", component:EzMusic}
  4. ])
  5. class EzApp{...}

RouteConfigAnnotation的构造函数参数与Routerconfig()方法参数一致,都是一个包含 若干配置项的数组。

事实上,它的确只是一个语法糖 : Angular2在bootstrap一个组件时,会检查组件是否存在 RouteConfig注解,如果存在,则利用这个信息调用Routerconfig()方法进行路由配置:

image_1b49bdtvagm1118bfon12jrul34o.png-23.9kB

不过这样写起来,感觉会流畅一些,声明式的氛围更强烈一些。

除了使用Routernavigate()方法切换路由,Angular2还提供了一个指令用来将一个DOM对象增强为路由入口:

  1. @Component({
  2. directives:[ROUTER_PROVIDERS]
  3. template : `<nav>
  4. <b [routerLink]="['Video']">video</b> |
  5. <b [routerLink]="['Music']">music</b>
  6. </nav>
  7. <router-outlet></router-outlet>`
  8. })

RouterLink指令为宿主DOM对象添加click事件监听,在触发时调用Routernavigate()方法进行路由。

路由项别名

需要指出的是,RouterLink并不能直接使用路由项的路径,routerLink属性的值是一个路由项的名称,我们需要在路由配置时通过name属性进行设定,必须遵循Pascal约定(首字母大写):

  1. @RouteConfig([
  2. {path:"/video", component:EzVideo , name:"Video"},
  3. {path:"/music", component:EzMusic , name:"Music"}
  4. ])

RouteRegistry - 路由注册表

Router通过config()进行的路由配置,保存在路由注册表中;而Router通过navigate()执行路由时,利用的就是路由注册表中的信息:

image_1b49bfhaaict1f4d1ra31fom19p755.png-10.2kB

在路由注册表中,保存有三种类型的路由表:

匹配表/matchers

匹配表是最基本的路由决策表,它使用正则表达式实现路径向组件的匹配。

  1. @RouteConfig([
  2. {path:"/video", component:EzVideo},
  3. {path:"/music", component:EzMusic}
  4. ])

对应生成的匹配表以正则表达式为键值,映射到汇算后的路由项:

  1. /^\/video$/g => {path:"/video", ...}
  2. /^\/music$/g => {path:"/music", ...}

重定向表/redirects

如果在配置时指定了路径重定向,那么将在重定向表中生成路由项。

  1. @RouteConfig([
  2. {path:"/video", component:EzVideo},
  3. {path:"/music", component:EzMusic},
  4. {path:"/test", redirectTo:"/video"}
  5. ])

对应生成的重定向表以原始路径为键,映射到目标路径:

  1. /test => /video

名称表/names

如果在配置路由时,通过name属性为路由项设置了名称,那么将为每个名称建立一个 路由项。我们已经知道,指令RouterLink需要使用名称表。

  1. @RouteConfig([
  2. {path:"/video", component:EzVideo,name:"Video"},
  3. {path:"/music", component:EzMusic,name:"Music"}
  4. ])

对应生成的名称表以名称为键值,映射到汇算后的路由项:

  1. video => {path:"/video", ...},
  2. music => {path:"/music", ...}

RouteParams - 路由参数

有时我们希望不同的URL能够路由到同一个组件,比如:

  1. /music/album/111
  2. /music/album/222

都路由到组件EzAlbum。

RouteConfig注解中声明路由项的URL时,可以使用:varname来标记一个路由参数:

  1. @RouteConfig([
  2. path:"/music/album/:aid",component:EzAlbum,name:"Music"
  3. ])
  4. class EzApp{...}

这满足了将/music/album/111/music/album/222都路由到EzAlbum组件的需求。

现在需要在EzAlbum组件中提取这个参数。

Angular2框架将路由参数封装为一个RouteParams对象,因此,我们在EzAlbum类的构造函数进行注入即可:

  1. class EzAlbum{
  2. constructor(@Inject(RouteParams) params){
  3. this.aid = params.aid;
  4. //do sth.
  5. }
  6. }
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注