Angular2 依赖注入的注入器与提供器

注入器(provider)与提供器(injector)是依赖注入必不可少的两个方面。

注入器(injector)

注入器的作用是将实例化的对象注入到组件中。

Angular 提供的服务类,一般情况下不需要直接调用注入器的方法,而是通过组件的构造函数将服务对象

product.component.ts

1
2
3
constructor(productService: ProductService){
...
}

注入器的层级关系

Angular 会首先创建一个「应用级的注入器」,把模块(module.ts)中声明的提供器(providers 数组)都注册到「应用级的注入器」上,还包括所有引用的模块(imports 数组)中所注册的提供器。

然后 Angular 会启动主组件,同时「应用级的注入器」会为主组件创建一个「组件级注入器」,并将组件中声明的提供器注册到「组件级的注入器」上。

如果主组件的模版引入了子组件,通过选择器方式或子路由引入子组件,子组件被创建时,父组件的注入器会为每个子组件创建子组件的注入器,然后把子组件的提供器注册到子组件的注入器上。

最后,所有的注入器会形成与组件关系相同的层级关系。

注入器的寻找策略

在使用时,组件会先查找自身是否注册了需要的提供器,如果没有,则对父组件进行查找,以此类推,直到「应用级注入器」的提供器,并根据找到的提供器的配置来实例化对象。

如果一直到应用级的注入器都没有找到符合条件的提供器,则会抛出异常。

注: Angular 只有一个注入点,即构造函数。因此,如果构造函数上没有注入,则没有引入任何服务。这是 Angular 与其它框架的差别之处。

手工创建注入器

在 Angular 中,注入器的寻找是完全由 Angular 自动完成的,我们不需要手工进行寻找。

下面我们来写如何手工创建一个注入器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { Component, OnInit, Injector } from '@angular/core';
import { HomeService } from '../service/home.service';

@Component({
...
})
export class HomeComponent implements OnInit {
homeService: HomeService;

constructor(public injector: Injector) {
this.homeService = injector.get(HomeService);
}
...
}

下面两种代码是等价的:

1
2
3
4
5
6
7
8
constructor(public injector: Injector) { 
this.homeService = injector.get(HomeService);
}

// 等价写法
constructor(public homeService: HomeService) {
...
}

通过上边的代码我们可以发现注入的本质,Angular 把自身注入器注入到组件的构造函数中,组件向注入器请求所需要的服务,注入进来的并不是 HomeService!而是通过调用 injector.get() 方法获得的。

注:应避免这种写法。原因在于很难通过参数知道真正注入的是什么服务。
除非我们无法判定需要注入哪些服务,才考虑用该种方法。

提供器(provider)

提供器是用于实例化对象。

useClass 方法

Angular 在 providers 寻找实例,如果找到,则会注入到 productService 对象中。

模块层级中 providers 的写法:

app.module.ts

1
2
3
4
5
// 简写方法
providers: [ProductService]

// 上边代码等价于
providers: [{provide: ProductService, useClass: ProductService}]

provide 指定了 token, useClass 为实例化的具体类名,使用的是 new 操作符实例化对象的方法。如果 provide 与 useClass 属性值同名,则可以简写,否则需要使用下面的方法:

1
providers: [{provide: ProductService, useClass: AnotherProductService}]

这种方法相当于使用 new 运算符实例化一个方法。

useFactory 方法

使用工厂方法注入,它会把工厂方法返回的实例注入 productService 属性中。

app.module.ts

1
providers: [{provide: ProductService, useFactory: () => {...}}]

useValue 方法

使用值注入,通过该方法可以将变量进行注入。

1
providers: [{provide: 'IS_DEV_ENV', useValue: false}]

使用new方法(useClass)声明提供器

下面通过一个简单的例子来实现一个服务的提供器和注入器。

首先用 Angular 命令行工具建立一个组件 home 和相应的服务 home.service。我们希望做的是通过 home.service 获得一个包含 id 和 name 的对象。

1
2
ng g component home
ng g service service/home

1. home.service.ts

服务的相关逻辑写在 service.ts 中,这里我们实现了一个 getInfo() 方法,可以获取一个 Info 的实例。

home.service.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
import { Injectable } from '@angular/core';

@Injectable()
export class HomeService {
constructor() { }
getInfo(): Info {
return new Info(1, 'Phoebe');
}
}

export class Info {
constructor(public id: number, public name: string) {}
}

2. home.component.ts

在 home 组件中利用构造函数使用服务。

home.component.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { Component, OnInit } from '@angular/core';
import { HomeService, Info } from '../service/home.service';

@Component({
...
})
export class HomeComponent implements OnInit {

private info: Info;

constructor(public homeService: HomeService) {
}

ngOnInit() {
this.info = this.homeService.getInfo();
}
}

3. home.component.html

模版中进行引用

1
2
3
<h2>依赖注入 Demo</h2>
<p>id: {{info.id}}</p>
<p>name: {{info.name}}</p>

4. app.module.ts

在 app.module.ts 中的 providers 进行声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
...
import { HomeService } from './service/home.service';

@NgModule({
declarations: [
...

],
imports: [
...
],
providers: [
...
HomeService
],
bootstrap: [AppComponent]
})
export class AppModule { }

下面我们来讨论一些更复杂的问题,首先就是提供器的作用域。

提供器的作用域

在上面的例子中,我们将提供器写在 app.module.ts 文件中,也就是模块级别的。事实上,提供器也可以声明在组件中。

我们建立一个新的组件 home2 和新的服务 AnotherHomeService 来演示这个例子。

1.another-home.service.ts

AnotherHomeService 实现了 HomeService 接口。

1
2
3
4
5
6
7
8
9
10
11
import { Injectable } from '@angular/core';
import { HomeService, Info } from './home.service';

@Injectable()
export class AnotherHomeService implements HomeService {
getInfo(): Info {
return new Info(2, 'Lily')
}

constructor() { }
}

2.home2.component.ts

在这个例子中,我们将 provider 声明在组件内,而没有在 app.module.ts 中进行声明。

在构造函数中,我们写的仍然是 constructor(public homeService: HomeService),因为它的类型是 HomeService,而非 AnotherHomeService。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { Component, OnInit } from '@angular/core';
import { HomeService, Info } from '../service/home.service';
import { AnotherHomeService } from '../service/another-home.service';

@Component({
...
// 声明在组件内的提供器
providers: [{provide: HomeService, useClass: AnotherHomeService}]
})
export class HomeComponent implements OnInit {

private info: Info;

constructor(public homeService: HomeService) {
}

ngOnInit() {
this.info = this.homeService.getInfo();
}
}

3. home2.component.html

模版中进行引用

1
2
3
<h2>依赖注入 Demo</h2>
<p>id: {{info.id}}</p>
<p>name: {{info.name}}</p>

打开浏览器,我们会发现已经注入了 AnotherHomeService:

总结

  • 当 provider 声明在模块(module)中,它是对所有组件可见的。例如 HomeComponent,虽然未在组件中声明,但是可用的。

  • 当 provider 声明在组件(component)中,它只对声明的组件和子组件可见。

  • 当 provider 同时声明在模块和组件中,组件中的 provider 会将模块中的 provider 覆盖。

  • provider 应优先声明在模块中,只有对某个组件不可见时,才应考虑声明在组件中。

Injectable()

我们会发现使用 AngularCli 工具创建组件时会在 component 中默认添加这样一行代码 @Injectable

home.service.ts

1
2
3
4
@Injectable()
export class HomeService {
...
}

Injectable() 是指当前服务中还可以依赖其他服务,也就是能够通过构造函数注入其他的服务。

注:这里并不是指当前服务是否能注入到其他服务中,当前服务能否注入到其他服务这是由是否在 provider 中声明决定的。

下面的例子实现了服务中(HomeService 服务)注入了其他服务(AnotherHomeService 服务):

home.service.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { Injectable } from '@angular/core';
import { AnotherHomeService } from './another-home.service';

@Injectable()
export class HomeService {

// 服务中注入另一个服务
constructor(public anotherHomeService: AnotherHomeService) { }

getInfo(): Info {
this.anotherHomeService.getInfo();
return new Info(1, 'Phoebe');
}
}

export class Info {
constructor(public id: number, public name: string) {}
}

总结

  • 只有声明了 @Injectable() 才可以注入其他服务,建议不管是否需要依赖其他服务,都加上该解释器。
  • @Component@Pipes 等都是 @Injectable() 的子类,因此无需声明也能注入其他的服务。

使用工厂方法(useFactory)声明提供器

上边的例子中我们都使用下面的语法来声明提供器,这种写法默认使用 new 操作符来创建实例。

1
providers: [{provide: HomeService, useClass: AnotherHomeService}]

如果我们服务对象无法用简单的 new 来进行实例化,例如在某些条件下实例化某个对象,或实例化对象时需要传递参数等,则需要使用工厂方法来声明一个提供器。

下面举个例子,实现根据随机数来随机声明 HomeService 或 AnotherHomeService 中的其中一种。

app.module.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
...
import { HomeService } from './service/home.service';
import { LoggerService } from './service/logger.service';
import { AnotherHomeService } from './service/another-home.service';

@NgModule({
declarations: [
...

],
imports: [
...
],
providers: [
...
{ provide: HomeService, useFactory: () => {
let logger = new LoggerService();
let dev = Math.random() > 0.5;
if(dev) {
return new HomeService(logger);
} else {
return new AnotherHomeService(logger);
}
}},
LoggerService
],
bootstrap: [AppComponent]
})
export class AppModule { }

我们在浏览器中不断刷新会发现随机输出不同的值,表明确实是随机调用了不同的 service 服务:

注:如果多个组件同时使用该工厂方法声明提供器,那么这些组件返回的随机结果是完全相同的。这意味着工厂方法创建出的是一个单例对象,它只会在第一个需要注入的对象时被调用一次,所有注入的实例都是同一个对象。

进一步思考上面的代码,我们在工厂方法内部使用了服务(HomeService和AnotherHomeService)依赖于另一个服务(LoggerService),我们手工实例化了一个服务(LoggerService),说明工厂方法与 LoggerService 是强耦合的。

事实上,我们工厂方法下方就声明了 LoggerService 的提供器。

1
2
3
4
providers: [
...,
LoggerService
]

如何在工厂方法中使用已声明的提供器呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
...
import { HomeService } from './service/home.service';
import { LoggerService } from './service/logger.service';
import { AnotherHomeService } from './service/another-home.service';

@NgModule({
declarations: [
...

],
imports: [
...
],
providers: [
...
{ provide: HomeService, useFactory: (logger: LoggerService) => {
let dev = Math.random() > 0.5;
if(dev) {
return new HomeService(logger);
} else {
return new AnotherHomeService(logger);
}
},deps: [LoggerService]},
LoggerService
],
bootstrap: [AppComponent]
})
export class AppModule { }

deps 的数组中注明了工厂方法的参数分别使用了哪些提供器。

当工厂方法被调用时,会根据 deps 数组中的 token 内容去找。在本例中,deps 的数组中只有一项 LoggerService,因此就会去寻找 LoggerService 在哪里声明,然后实例化该对象,返回给服务的参数。

注:deps 中依赖的服务,仍然可以是工厂方法创建的,因此可能存在一个深层的依赖关系,而组件并不需要关心这其中的依赖关系,它们被屏蔽在组件之外。

使用值(useValue)方法声明提供器

我们在实际项目中不可能依赖一个随机数来决定使用哪个服务,通常情况下,我们可能会依赖某个变量的值。那么提供器中可以注入变量吗?答案是肯定的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
...
import { HomeService } from './service/home.service';
import { LoggerService } from './service/logger.service';
import { AnotherHomeService } from './service/another-home.service';

@NgModule({
declarations: [
...

],
imports: [
...
],
providers: [
...
{ provide: HomeService, useFactory: (logger: LoggerService, isDev ) => {
console.log(isDev)
},deps: [LoggerService, "IS_DEV_ENV"]},
LoggerService,
{provide: 'IS_DEV_ENV', useValue: false}
],
bootstrap: [AppComponent]
})
export class AppModule { }

注入之前,使用变量来声明一个提供器,即用值(useValue)来声明一个提供器:

1
{provide: 'IS_DEV_ENV', useValue: false}

然后我们在 deps 数组中追加一项:

1
deps: [LoggerService, "IS_DEV_ENV"]}

就可以在 useFactory 中以参数的形式传入了。