Angular2 依赖注入

前言

这是Angular2教程的第三部分,主要介绍Angular2的service的使用,以及ood在angular2里面的实现方式。相关博客如下:

  1. Angular2 初探
  2. Angular2 表单验证
  3. Angular2之rxjs以及http的世界
  4. Angular2 cheatsheet

参考资料

还是小G上个博客里面介绍的例子,链接在这:Angular 2 quick start。文件结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
├──angular2/                                    
│ │
│ ├──app/
│ │ ├──boot.ts
│ │ ├──app.component.ts
│ │ ├──hero-list/ //依赖注入 用
│ │ ├──edit-item.ts
│ │ ├──hero-card.component.ts
│ │ ├──hero-editor.component.ts
│ │ ├──hero.ts
│ │ ├──heros-list.component.ts
│ │ ├──heroes.service.ts
│ │ ├──restore.service.ts
│ ├──index.html
│ ├──package.json //用来安装我们需要的库, 以及node的一些命令
│ ├──tsconfig.json //用来配置typescript
│ ├──style.css //这个playground的css

在这篇博客里主要会用到的是hero-list这个模块,这个模块的主要作用做一个可更改的英雄名称,并且带有保存和取消的功能。保存需要保存当前的更改,而取消则需要恢复更改之前的名字,当已经更改了英雄的名称但是取消了之后这个会返回到更改之前的结果。

依赖注入介绍

在OOD中,一般一个class要初始化的时候,construct里面会有这个class需要依赖的class,比如我们要造一辆车,我们需要引擎和轮胎。我们可以这么做:

1
2
3
4
5
6
7
8
class Car{
private engine:Engine;
private tires:Tires;
constructor(){
this.engine = new Engine();
this.tires=new Tires();
}
}

但是这并不是一个好的ood的方法,因为一辆车有很多的引擎,有不同的轮胎,每次我们要初始化不同引擎不同轮胎的车我们都要改constructor去使用不同的引擎不同的轮胎。所以这种设计没有可扩展性。所以我们可以一般的OOD的方法是把这个class需要的东西放入constructor的参数中,如

1
2
3
4
5
6
7
8
class Car{
private engine:Engine;
private tires:Tires;
constructor(engine: Engine, tires: Tires) {
this.engine = engine;
this.tires = tires;
}
}

然后我们初始化对象的时候,可以这么做:

1
var car = new Car(new Engine(), new Tires());

这样车的类就基于引擎的类以及轮胎的类,而这也有很好的扩展性,当我们想建造不同的车的时候,我们可以使用不同的引擎以及不同的轮胎,只有在初始化对象的时候需要明确引擎以及轮胎的类就行。在Angular2里面引用了同样地思路。在第一篇博客中提到directive和providers的概念,其中directive是用来包括子模块,但是父模块一般不会使用子模块的逻辑或者方法,子模块对于父模块来说相当于一个有输入输出接口的黑盒,它只给它的父模块一个名字(定义在子模块的selector里),然后父模块在模板里面引用子模块,并且给予输入输出。然而如果两个模块想共享某些逻辑的时候该如何呢?这时候就用到service的概念了。service就好象c++里面的接口一样,使得不同的模块之间能够通信。但我们需要给让一个模块使用service的时候,有两种方法:

  1. 在bootstrap函数里面加入根模块,然后加上service component。如:
    1
    bootstrap(AppComponent, [HeroService]);

这样就把这个service加入到整个app里面。然后这个app component包括它所有的子模块都能使用 HeroService这个service。

1
2
3
4
5
     A
|
B----C---D
|
E

比如上图这个结构,A是根模块,然后有BCD三个子模块,然后B还有E这个子模块。ABCDE都能使用用bootstrap函数加入的service。但有一点要注意的是,在Angular2里面所有的service是单例的。单例在OOD里面的意思是整个app只能有一个对象,在Angular2里面同样是这个意思,如果把service加在根模块,虽然所有的子模块都共享这个service,但是它产生的service是单例的,只有一个子模块在runtime能使用这个service。这样就会产生一个问题,比如我有一个保存的service用来保存当前值,然后BCD这三个模块都需要保存的功能,但是由于service是单例的,它只能保存一个值,这样就会产生问题。解决办法就是模块使用service的第二种方法:

  1. 在component的providers里面直接加入service,如:
    1
    2
    3
    4
    @Component({
    selector: 'hero-editor',
    providers: [RestoreService]
    })

然后在类的constructor里面加入这个service:

1
constructor(private restoreService: RestoreService<Hero>) {}

这样之后虽然每个service是单例的,但是由于每个模块有自己的service,所以这个service互相不干扰,每个模块可以有自己的保存功能。在类里面可以用this.restoreService来使用这个service的实例。
而我们在写service的时候,一般没有@Component了,只有一些函数,以及实现逻辑。一般可以做成service的可以是模块之间的信息交流,也可以是模块对外拿data,也可以是一个可扩展可共享的函数逻辑,比如我们要做还原功能,我们可以有一个restore service:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export class RestoreService<T> {
originalItem: T;
currentItem: T;

setItem (item: T) {
this.originalItem = item;
this.currentItem = this.clone(item);
}

getItem () :T {
return this.currentItem;
}

restoreItem () :T {
this.currentItem = this.originalItem;
return this.getItem();
}

clone (item: T) :T {
// super poor clone implementation
return JSON.parse(JSON.stringify(item));
}
}

因为每个模块都需要这个service,所以需要用providers来吧service引入到component里面。
但如果一个service要用另外一个service应该怎么做呢?由于service是一个类,所以我们并不需要引入providers,我们只需要import相应的子service然后在父service的constructor中加入这个Service,比如如果我们想要在restore service里面加入一个logger service,我们只需要:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import {Logger} from './logger';
import {Injectable} from 'angular2/angular2';

@Injectable()
export class RestoreService<T> {
constructor(private logger: Logger) {

}

restoreItem () :T {
this.currentItem = this.originalItem;
this.logger.log('this is restore item');
return this.getItem();
}
}

这里有一点要注意是 @Injectable() 这个语法。这个不是可省略的。一定要加入这个@Injectable(),一定要加入这个@Injectable(),一定要加入这个@Injectable()。重要的事情说三遍。因为Angular2需要这个语法来把logger插入到restore service里面,如果没有这个@Injectable(), Angular2就不知道要插入这个Logger service,
就会报错。大家可能会有疑问,那为啥在component里面加入restore service的时候不需要加入@Injectable呢?这是因为component已经用@Component表示了,所以在这里可以省略。
在依赖注入里面,还有一点是service的provider可以是类,可以是值,也可以是工厂类型。
service的provider是类的例子上面已经给出了,如果provider是值的例子如下:

1
2
3
4
beforeEachProviders(() => {
let emptyHeroService = { getHeroes: () => [] };
return [ provide(HeroService, {useValue: emptyHeroService}) ];
});

这个一般用于Jasmine测试里。
如果provider是一个工厂类型,这更符合扩展性的要求。比如我们有A service,依赖于B service,但我们同时在C service某个参数为真时使用这个service,我们就可以这么写:

1
2
3
let AServiceFactory = (b: B, c: C) => {
return new HeroService(b, c.isSpecial);
}

这样一来,只有c.isSpecial 是真的时候我们才使用C的service。
如果我们要用工厂类型的时候,可以定义一个definition:

1
2
3
4
5
6
let ADefinition = {
useFactory: AServiceFactory,
deps: [B, C]
};
let AServiceProvider = provide(A, ADefinition);
bootstrap(AppComponent, [AServiceProvider, B, C]);

具体实例

在hero-list这个例子中,有两个service,一个是heros.service,用来保存现有的英雄名字以及超能力。如果要扩展可以用restful api来得到英雄的名字以及超能力。另外一个是restore.service,用于保存更改之前的结果,如果按取消就返回保存之前的结果。
还有一个hero的类,由于是简单版本,所以只有名字和超能力的元素。
接下来是hero-editor.component以及hero-card.component,这两个模块是hero-list.component这个模块的两个子模块,在hero-list.component里面对于每个从heroes.service里面得到的英雄,都包含hero-editor.component来编辑英雄的名字以及hero-card.component
来显示更改之后的内容。
在hero-list.component里可以看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
< li *ngFor="#editItem of heroes">
< hero-card
[hidden]="editItem.editing"
[hero]="editItem.item">

< /hero-card>
< button
[hidden]="editItem.editing"
(click)="editItem.editing = true">

edit
< /button>
< hero-editor
(saved)="onSaved(editItem, $event)"
(canceled)="onCanceled(editItem)"
[hidden]="!editItem.editing"
[hero]="editItem.item">

< /hero-editor>
< /li>

对于hero-editor这个component,它定义了(saved)和(canceled)这两个@Output,在按按键的时候发出这个事件,并且用restore service来更改内容,然后hero-list.component监听到这个事件,做除相应的保存或者返回操作。在这里要注意一点的是,在第一章博客我提到了用output传递参数,
在这里我们可以看到在保存的时候,在hero-editor.component里:

1
2
3
onSaved () {
this.saved.next(this.restoreService.getItem());
}

它把这个当前的值传递了出去,而在hero-list.component监听的时候,

1
2
3
4
5
6
7
8
9
10
11
< hero-editor
(saved)="onSaved(editItem, $event)"
(canceled)="onCanceled(editItem)"
[hidden]="!editItem.editing"
[hero]="editItem.item">

< /hero-editor>

onSaved (editItem: EditItem<Hero>, updatedHero: Hero) {
editItem.item = updatedHero;
editItem.editing = false;
}

参数通过event传递了过来,并且可以直接使用。在这里type就是传递时候的type。
再这个例子里,只有hero-editor基于restore的service,所以只有hero-editor加入了restore的service。由于有多个hero-editor,所以多个service会有多个实例。而heroes-service只需要在主模块里面使用,所以他是一个单例模式。

小结

这一个博客主要介绍了一下依赖注入,主要有下面几个知识点:

  1. 在模块中加入service有两种方法,但由于service是单例模式的,所以不同的方法会有不同的行为,总体来说如果在app里面只需要一个实例存在可以把service加载根模块中,但如果每个模块都需要这个service则需要使用providers来插入service。
  2. service的provider可以不仅仅是类,也可是值或者是工厂。
  3. 如果在service里面加入service需要@Injectable。