目录
依赖注入(DI)
Angular的依赖注入系统能够即时地创建和交付所依赖的服务。
“依赖注入”是提供类的新实例的一种方式,还负责处理好类所需的全部依赖。大多数依赖都是服务。 Angular 使用依赖注入来提供新组件以及组件所需的服务。
Angular 通过查看构造函数的参数类型得知组件需要哪些服务。 例如,HeroListComponent组件的构造函数需要一个HeroService服务:
constructor(private service: HeroService) { }
当 Angular 创建组件时,会首先为组件所需的服务请求一个注入器 (injector)。
注入器维护了一个服务实例的容器,存放着以前创建的实例。 如果所请求的服务实例不在容器中,注入器就会创建一个服务实例,并且添加到容器中,然后把这个服务返回给 Angular。 当所有请求的服务都被解析完并返回时,Angular 会以这些服务为参数去调用组件的构造函数。 这就是依赖注入 。
必须在要求注入HeroService
之前,在注入器中注册HeroService
的提供商 Provider。 提供商用于创建并返回一个服务,通常是服务类本身。通过注册提供商,注入器才可以在没有服务的情况下,根据提供商创建对应的服务。
为什么需要依赖注入?
- 依赖注入是一种编程模式,可以让类从外部源中获得它的依赖,而不必亲自创建它们。
Angular 依赖注入
- Angular 附带了自己的依赖注入框架。此框架也能被当做独立模块用于其它应用和框架中。
- 通过像如下的代码一样创建服务:
import { Injectable } from '@angular/core';
import { HEROES } from './mock-heroes';
@Injectable()
export class HeroService {
getHeroes() { return HEROES; }
}
如果真的从远端服务器获取数据,这个 API 必须是异步的,可能得返回 ES2015 承诺 (promise)。 需要重新处理组件消费该服务的方式。
3. 服务只是 Angular 中的一个类。 有 Angular 注入器注册它之前,没有任何特别之处。
配置注入器
不需要创建 Angular 注入器。 Angular 在启动过程中自动为我们创建一个应用级注入器。
platformBrowserDynamic().bootstrapModule(AppModule);
我们必须通过注册提供商 (provider)
来配置注入器,这些提供商为应用创建所需服务。
或者在 NgModule 中注册提供商,或者在应用组件中。
在 NgModule 中注册提供商
通常会把提供商添加到根模块上,以便在任何地方使用服务的同一个实例。
如在AppModule中注册Logger
、UserService
和APP_CONFIG
提供商。
app/app.module.ts
@NgModule({
imports: [
BrowserModule
],
declarations: [
AppComponent,
/* . . . */
],
providers: [
UserService,
{ provide: APP_CONFIG, useValue: HERO_DI_CONFIG }
],
bootstrap: [ AppComponent ]
})
export class AppModule { }
在组件中注册提供商
或者,也可以在@Component元数据中的providers属性中把它注册在组件层:
下面是更新的HerosComponent,它注册了HeroService。
import { Component } from '@angular/core';
import { HeroService } from './hero.service';
@Component({
selector: 'my-heroes',
providers: [HeroService],
template: `
<h2>Heroes</h2>
<hero-list></hero-list>
`
})
export class HeroesComponent { }
注-1:把它注册在组件级表示该组件的每一个新实例都会有一个服务的新实例。
注-2:如果忘了注册提供商时,Angular回在首次查找该服务时,抛出一个异常,如:
EXCEPTION: No provider for Logger! (HeroListComponent -> HeroService -> Logger)
(异常:Logger类没有提供商!(HeroListComponent -> HeroService -> Logger))
该用NgModule还是应用组件?
一方面,NgModule 中的提供商是被注册到根注入器。这意味着在 NgModule 中注册的提供商可以被整个应用访问。
另一方面,在应用组件中注册的提供商只在该组件及其子组件中可用。
注入服务
遵照依赖注入模式的要求,组件必须在它的构造函数中请求这些服务。
export class HeroListComponent {
heroes: Hero[];
constructor(heroService: HeroService) {
this.heroes = heroService.getHeroes();
}
}
显性注入器的创建
injector = ReflectiveInjector.resolveAndCreate([Car, Engine, Tires]);
let car = injector.get(Car);
在必要时,可以写使用显式注入器的代码,但却很少这样做。 当 Angular 创建组件时 —— 无论通过像这样的 HTML 标签还是通过路由导航到组件 —— 它都会自己管理好注入器的创建和调用。
单例服务
在一个注入器的范围内,依赖都是单例的。 在这个例子中,HeroesComponent
和它的子组件HeroListComponent
共享同一个HeroService
实例。
然而,Angular DI 是一个分层的依赖注入系统,这意味着嵌套的注入器可以创建它们自己的服务实例。
当服务需要别的服务时
如果HeroService
也有依赖,需要通过日志服务来汇报自己的活动呢?我们同样用构造函数注入模式,来添加一个带有Logger
参数的构造函数。
未注入Logger服务:
import { Injectable } from '@angular/core';
import { HEROES } from './mock-heroes';
@Injectable()
export class HeroService {
getHeroes() { return HEROES; }
}
注入Logger服务:
import { Injectable } from '@angular/core';
import { HEROES } from './mock-heroes';
import { Logger } from '../logger.service';
@Injectable()
export class HeroService {
constructor(private logger: Logger) { }
getHeroes() {
this.logger.log('Getting heroes ...');
return HEROES;
}
}
为什么要用 @Injectable()?
- @Injectable() 标识一个类可以被注入器实例化。 通常,在试图实例化没有被标识为
@Injectable()
的类时,注入器会报错。(我们可以理解为用@Injectable标识的类可以被注入服务) - 建议为每个服务类都添加
@Injectable()
,包括那些没有依赖严格来说并不需要它的。因为:
- 面向未来:没有必要记得在后来添加依赖的时候添加
@Injectable()
。 - 一致性:所有的服务都遵循同样的规则,不需要考虑为什么某个地方少了一个。
- 面向未来:没有必要记得在后来添加依赖的时候添加
注:注入器同时负责实例化像HerosComponent这样的组件。为什么不标记HerosComponent为@Injectable()呢?
我们可以添加它。但是没有必要,因为HerosComponent已经有@Component装饰器了, @Component(和随后将会学到的@Directive和@Pipe一样)是 Injectable 的子类型。 实际上,正是这些Injectable装饰器是把一个类标识为注入器实例化的目标。
- 在运行时,注入器可以从编译后的 JavaScript 代码中读取类的元数据,并使用构造函数的参数类型信息来决定注入什么。
注:总是使用@Injectable()
的形式,不能只用@Injectable
。 如果忘了括号,应用就会神不知鬼不觉的失败!
注入器的提供商们
- 提供商提供依赖值的一个具体的、运行时的版本。
- 注入器依靠提供商创建服务的实例,注入器再将服务的实例注入组件或其它服务。
- 必须为注入器注册一个服务的提供商,否则它不知道该如何创建该服务。
Provider类和provide对象常量
像下面一样写providers
数组:
providers: [Logger]
这其实是用于注册提供商的简写表达式。 使用的是一个带有两个属性的提供商
对象字面量:
[{ provide: Logger, useClass: Logger }]
第一个是令牌 (token),它作为键值 (key) 使用,用于定位依赖值和注册提供商。
第二个是供应商定义对象。 可以把它看做是指导如何创建依赖值的配方。 有很多方式创建依赖值…… 也有很多方式可以写配方。
备选的类提供商
某些时候,我们会请求一个不同的类来提供服务。 下列代码告诉注入器,当有人请求Logger时,返回BetterLogger。
[{ provide: Logger, useClass: BetterLogger }]
带依赖的类提供商
假设EvenBetterLogger可以在日志消息中显示用户名。 这个日志服务从注入的UserService中取得用户, UserService通常也会在应用级注入。
@Injectable()
class EvenBetterLogger extends Logger {
constructor(private userService: UserService) { super(); }
log(message: string) {
let name = this.userService.user.name;
super.log(`Message to ${name}: ${message}`);
}
}
就像之前在BetterLogger中那样配置它。
[ UserService,
{ provide: Logger, useClass: EvenBetterLogger }]
别名类提供商
服务OldLogger和NewLogger具有相同接口。当旧组件想使用OldLogger
记录消息时,我们希望改用NewLogger的单例对象来记录。
如果采用如下方式(备选的类提供商):
[ NewLogger,
// Not aliased! Creates two instances of `NewLogger`
{ provide: OldLogger, useClass: NewLogger}]
那么,应用中将会出现两个不同的NewLogger
实例。但是我们要求:不管组件请求的是新的还是旧的日志服务,依赖注入器注入的都应该是同一个单例对象。这时,我们应该使用useExisting
选项指定别名。
[ NewLogger,
// Alias OldLogger w/ reference to NewLogger
{ provide: OldLogger, useExisting: NewLogger}]
值提供商
有时,提供一个预先做好的对象会比请求注入器从类中创建它更容易。
// An object in the shape of the logger service
let silentLogger = {
logs: ['Silent logger says "Shhhhh!". Provided via "useValue"'],
log: () => {}
};
于是可以通过useValue
选项来注册提供商,它会让这个对象直接扮演 logger 的角色。
[{ provide: Logger, useValue: silentLogger }]
工厂提供商
当服务的构造参数需要传入不能注入的类型的参数时,如果在providers处配置服务提供商的话,使用服务时将会出现错误,原因是创建该服务时缺少参数。如:
hero.service.ts
constructor(
private logger: Logger,
private isAuthorized: boolean) { }
getHeroes() {
let auth = this.isAuthorized ? 'authorized ' : 'unauthorized';
this.logger.log(`Getting heroes for ${auth} user.`);
return HEROES.filter(hero => this.isAuthorized || !hero.isSecret);
}
我们可以注入Logger
,但是不能注入逻辑型的isAuthorized
。 我们不得不通过通过工厂提供商创建这个HeroService
的新实例。
hero.service.provider.ts
// 工厂提供商需要一个工厂方法:
let heroServiceFactory = (logger: Logger, userService: UserService) => {
return new HeroService(logger, userService.user.isAuthorized);
};
// 同时把Logger和UserService注入到工厂提供商中,并且让注入器把它们传给工厂方法
export let heroServiceProvider =
{ provide: HeroService,
useFactory: heroServiceFactory,
deps: [Logger, UserService]
};
注:useFactory字段告诉 Angular:这个提供商是一个工厂方法,它的实现是heroServiceFactory。
deps属性是提供商令牌数组。 Logger和UserService类作为它们自身类提供商的令牌。 注入器解析这些令牌,把相应的服务注入到工厂函数中相应的参数中去。
HeroService提供商
import { Component } from '@angular/core';
import { HeroService } from './hero.service';
@Component({
selector: 'my-heroes',
providers: [HeroService],
template: `
<h2>Heroes</h2>
<hero-list></hero-list>
`
})
export class HeroesComponent { }
heroServiceFactory工厂提供商
import { Component } from '@angular/core';
import { heroServiceProvider } from './hero.service.provider';
@Component({
selector: 'my-heroes',
template: `
<h2>Heroes</h2>
<hero-list></hero-list>
`,
providers: [heroServiceProvider]
})
export class HeroesComponent { }
依赖注入令牌
当向注入器注册提供商时,实际上是把这个提供商和一个 DI 令牌关联起来了。 注入器维护一个内部的令牌-提供商映射表,这个映射表会在请求依赖时被引用到。 令牌就是这个映射表中的键值。
在前面的所有例子中,依赖值都是一个类实例,并且类的类型作为它自己的查找键值。 在下面的代码中,HeroService类型作为令牌,直接从注入器中获取HeroService 实例:
heroService: HeroService = this.injector.get(HeroService);
注:这里的令牌指的是Provider对象的provide属性的值,通过该值来查找到所需的依赖。
非类依赖
如果依赖值不是一个类呢?有时候想要注入的东西是一个字符串,函数或者对象。
应用程序经常为很多很小的因素定义配置对象(例如应用程序的标题或网络API终点的地址)。 但是这些配置对象不总是类的实例,它们可能是对象,如下面这个:
app-config.ts
export interface AppConfig {
apiEndpoint: string;
title: string;
}
export const HERO_DI_CONFIG: AppConfig = {
apiEndpoint: 'api.heroes.com',
title: 'Dependency Injection'
};
我们想让这个配置对象在注入时可用,而且知道可以使用值提供商来注册一个对象。但是,这种情况下用什么作令牌呢?
注:可能有人会想到使用AppConfig作为令牌。但是TypeScript 接口不是一个有效的令牌。
CONFIG
常量有一个接口:AppConfig
。不幸的是,不能把 TypeScript 接口用作令牌:
// FAIL! Can't use interface as provider token
[{ provide: AppConfig, useValue: HERO_DI_CONFIG })]
// FAIL! Can't inject using the interface as the parameter type
constructor(private config: AppConfig){ }
对于习惯于在强类型的语言中使用依赖注入的开发人员,这会看起来很奇怪, 因为在强类型语言中,接口是首选的用于查找依赖的主键。
这不是 Angular 的错。接口只是 TypeScript 设计时 (design-time) 的概念。JavaScript 没有接口。 TypeScript 接口不会出现在生成的 JavaScript 代码中。 在运行期,没有接口类型信息可供 Angular 查找。
OpaqueToken
解决方案是定义和使用 OpaqueToken(不透明的令牌)。定义方式类似于这样:
import { OpaqueToken } from '@angular/core';
export let APP_CONFIG = new OpaqueToken('app.config');
使用这个OpaqueToken
对象注册依赖的提供商:
providers: [{ provide: APP_CONFIG, useValue: HERO_DI_CONFIG }]
现在,在@Inject
装饰器的帮助下,这个配置对象可以注入到任何需要它的构造函数中:
constructor(@Inject(APP_CONFIG) config: AppConfig) {
this.title = config.title;
}
注:虽然ConfigAppConfig
接口在依赖注入过程中没有任何作用,但它为该类中的配置对象提供了强类型信息。
或者在 ngModule 中提供并注入这个配置对象,如AppModule。
app/app.module.ts
providers: [
UserService,
{ provide: APP_CONFIG, useValue: HERO_DI_CONFIG }
],
可选依赖
HeroService需要一个Logger,但是如果想不提供 Logger 也能得到它,该怎么办呢? 可以把构造函数的参数标记为@Optional(),告诉 Angular 该依赖是可选的:
import { Optional } from '@angular/core';
HeroService中
constructor(@Optional() private logger: Logger) {
if (this.logger) {
this.logger.log(some_message);
}
}
当使用@Optional()时,代码必须准备好如何处理空值。 如果其它的代码没有注册一个 logger,注入器会设置该logger的值为空 null。
附录:为什么建议每个文件只放一个类
在同一个文件中有多个类容易造成混淆,最好避免。 开发人员期望每个文件只放一个类。
如果我们蔑视这个建议,并且 —— 比如说 —— 把HeroService和HeroesComponent组合在同一个文件里,就得把组件定义放在最后面! 如果把组件定义在了服务的前面, 在运行时抛出空指针错误。
注:在forwardRef()方法的帮助下,实际上也可以先定义组件。 但是为什么要先给自己找麻烦呢? 还是通过在独立的文件中定义组件和服务,完全避免此问题吧。