Angular9 依赖注入
依赖注入(DI
)是一种重要的应用设计模式。 Angular 有自己的 DI
框架,在设计应用时常会用到它,以提升它们的开发效率和模块化程度。
依赖,是当类需要执行其功能时,所需要的服务或对象。 DI
是一种编码模式,其中的类会从外部源中请求获取依赖,而不是自己创建它们。
在 Angular 中,DI
框架会在实例化该类时向其提供这个类所声明的依赖项。本指南介绍了 DI
在 Angular 中的工作原理,以及如何借助它来让你的应用更灵活、高效、健壮,以及可测试、可维护。
我们先看一下英雄指南中英雄管理特性的简化版。这个简化版不使用 DI
,我们将逐步把它转换成使用 DI
的。
- Path:"src/app/heroes/heroes.component.ts" 。
import { Component } from '@angular/core';
@Component({
selector: 'app-heroes',
template: `
<h2>Heroes</h2>
<app-hero-list></app-hero-list>
`
})
export class HeroesComponent { }
- Path:"src/app/heroes/heroes.component.ts" 。
import { Component } from '@angular/core';
import { HEROES } from './mock-heroes';
@Component({
selector: 'app-hero-list',
template: `
<div *ngFor="let hero of heroes">
{{hero.id}} - {{hero.name}}
</div>
`
})
export class HeroListComponent {
heroes = HEROES;
}
- Path:"src/app/heroes/hero.ts" 。
export interface Hero {
id: number;
name: string;
isSecret: boolean;
}
- Path:"src/app/heroes/mock-heroes.ts" 。
import { Hero } from './hero';
export const HEROES: Hero[] = [
{ id: 11, isSecret: false, name: 'Dr Nice' },
{ id: 12, isSecret: false, name: 'Narco' },
{ id: 13, isSecret: false, name: 'Bombasto' },
{ id: 14, isSecret: false, name: 'Celeritas' },
{ id: 15, isSecret: false, name: 'Magneta' },
{ id: 16, isSecret: false, name: 'RubberMan' },
{ id: 17, isSecret: false, name: 'Dynama' },
{ id: 18, isSecret: true, name: 'Dr IQ' },
{ id: 19, isSecret: true, name: 'Magma' },
{ id: 20, isSecret: true, name: 'Tornado' }
];
HeroesComponent
是顶层英雄管理组件。 它唯一的目的是显示 HeroListComponent
,该组件会显示一个英雄名字的列表。
HeroListComponent
的这个版本从 HEROES 数组(它在一个独立的 "mock-heroes" 文件中定义了一个内存集合)中获取英雄。
Path:"src/app/heroes/hero-list.component.ts (class)" 。
export class HeroListComponent {
heroes = HEROES;
}
这种方法在原型阶段有用,但是不够健壮、不利于维护。 一旦你想要测试该组件或想从远程服务器获得英雄列表,就不得不修改 HeroesListComponent
的实现,并且替换每一处使用了 HEROES
模拟数据的地方。
创建和注册可注入的服务
DI
框架让你能从一个可注入的服务类(独立文件)中为组件提供数据。为了演示,我们还会创建一个用来提供英雄列表的、可注入的服务类,并把它注册为该服务的提供者。
同一个文件中放多个类容易让人困惑。我们通常建议你在单独的文件中定义组件和服务。
如果你把组件和服务都放在同一个文件中,请务必先定义服务,然后再定义组件。如果在服务之前定义组件,则会在运行时收到一个空引用错误。
也可以借助
forwardRef()
方法来先定义组件,就像这个博客中解释的那样。
创建可注入的服务类
Angular CLI 可以用下列命令在 "src/app/heroes" 目录下生成一个新的 HeroService
类。
ng generate service heroes/hero
下列命令会创建 HeroService
的骨架。
Path:"src/app/heroes/hero.service.ts (CLI-generated)" 。
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class HeroService {
constructor() { }
}
@Injectable()
是每个 Angular 服务定义中的基本要素。该类的其余部分导出了一个 getHeroes
方法,它会返回像以前一样的模拟数据。(真实的应用可能会从远程服务器中异步获取这些数据,不过这里我们先忽略它,专心实现服务的注入机制。)
Path:"src/app/heroes/hero.service.ts" 。
import { Injectable } from '@angular/core';
import { HEROES } from './mock-heroes';
@Injectable({
// we declare that this service should be created
// by the root application injector.
providedIn: 'root',
})
export class HeroService {
getHeroes() { return HEROES; }
}
用服务提供者配置注入器
我们创建的类提供了一个服务。@Injectable()
装饰器把它标记为可供注入的服务,不过在你使用该服务的 provider
提供者配置好 Angular 的依赖注入器之前,Angular 实际上无法将其注入到任何位置。
该注入器负责创建服务实例,并把它们注入到像 HeroListComponent
这样的类中。 你很少需要自己创建 Angular 的注入器。Angular 会在执行应用时为你创建注入器,第一个注入器是根注入器,创建于启动过程中。
提供者会告诉注入器如何创建该服务。 要想让注入器能够创建服务(或提供其它类型的依赖),你必须使用某个提供者配置好注入器。
提供者可以是服务类本身,因此注入器可以使用 new
来创建实例。 你还可以定义多个类,以不同的方式提供同一个服务,并使用不同的提供者来配置不同的注入器。
注入器是可继承的,这意味着如果指定的注入器无法解析某个依赖,它就会请求父注入器来解析它。 组件可以从它自己的注入器来获取服务、从其祖先组件的注入器中获取、从其父
NgModule
的注入器中获取,或从root
注入器中获取。
你可以在三种位置之一设置元数据,以便在应用的不同层级使用提供者来配置注入器:
- 在服务本身的
@Injectable()
装饰器中。
- 在
NgModule
的@NgModule()
装饰器中。
- 在组件的
@Component()
装饰器中。
@Injectable()
装饰器具有一个名叫 providedIn
的元数据选项,在那里你可以指定把被装饰类的提供者放到 root
注入器中,或某个特定 NgModule
的注入器中。
@NgModule()
和 @Component()
装饰器都有用一个 providers
元数据选项,在那里你可以配置 NgModule
级或组件级的注入器。
所有组件都是指令,而
providers
选项是从@Directive()
中继承来的。 你也可以与组件一样的级别为指令、管道配置提供者。
注入服务
HeroListComponent
要想从 HeroService
中获取英雄列表,就得要求注入 HeroService
,而不是自己使用 new
来创建自己的 HeroService
实例。
你可以通过制定带有依赖类型的构造函数参数来要求 Angular 在组件的构造函数中注入依赖项。下面的代码是 HeroListComponent
的构造函数,它要求注入 HeroService
。
Path:"src/app/heroes/hero-list.component (constructor signature)" 。
constructor(heroService: HeroService)
当然,HeroListComponent
还应该使用注入的这个 HeroService
做一些事情。 这里是修改过的组件,它转而使用注入的服务。与前一版本并列显示,以便比较。
//hero-list.component (with DI)
import { Component } from '@angular/core';
import { Hero } from './hero';
import { HeroService } from './hero.service';
@Component({
selector: 'app-hero-list',
template: `
<div *ngFor="let hero of heroes">
{{hero.id}} - {{hero.name}}
</div>
`
})
export class HeroListComponent {
heroes: Hero[];
constructor(heroService: HeroService) {
this.heroes = heroService.getHeroes();
}
}
//hero-list.component (without DI)
import { Component } from '@angular/core';
import { HEROES } from './mock-heroes';
@Component({
selector: 'app-hero-list',
template: `
<div *ngFor="let hero of heroes">
{{hero.id}} - {{hero.name}}
</div>
`
})
export class HeroListComponent {
heroes = HEROES;
}
必须在某些父注入器中提供 HeroService
。HeroListComponent
并不关心 HeroService
来自哪里。 如果你决定在 AppModule
中提供 HeroService
,也不必修改 HeroListComponent
。
注入器树与服务实例
在某个注入器的范围内,服务是单例的。也就是说,在指定的注入器中最多只有某个服务的最多一个实例。
应用只有一个根注入器。在 root
或 AppModule
级提供 UserService
意味着它注册到了根注入器上。 在整个应用中只有一个 UserService
实例,每个要求注入 UserService
的类都会得到这一个服务实例,除非你在子注入器中配置了另一个提供者。
Angular DI 具有分层注入体系,这意味着下级注入器也可以创建它们自己的服务实例。 Angular 会有规律的创建下级注入器。每当 Angular 创建一个在 @Component()
中指定了 providers
的组件实例时,它也会为该实例创建一个新的子注入器。 类似的,当在运行期间加载一个新的 NgModule
时,Angular 也可以为它创建一个拥有自己的提供者的注入器。
子模块和组件注入器彼此独立,并且会为所提供的服务分别创建自己的实例。当 Angular 销毁 NgModule
或组件实例时,也会销毁这些注入器以及注入器中的那些服务实例。
借助注入器继承机制,你仍然可以把全应用级的服务注入到这些组件中。 组件的注入器是其父组件注入器的子节点,它会继承所有的祖先注入器,其终点则是应用的根注入器。 Angular 可以注入该继承谱系中任何一个注入器提供的服务。
比如,Angular 既可以把 HeroComponent
中提供的 HeroService
注入到 HeroListComponent
,也可以注入 AppModule
中提供的 UserService
。
测试带有依赖的组件
基于依赖注入设计一个类,能让它更易于测试。 要想高效的测试应用的各个部分,你所要做的一切就是把这些依赖列到构造函数的参数表中而已。
比如,你可以使用一个可在测试期间操纵的模拟服务来创建新的 HeroListComponent
。
Path:"src/app/test.component.ts" 。
const expectedHeroes = [{name: 'A'}, {name: 'B'}]
const mockService = <HeroService> {getHeroes: () => expectedHeroes }
it('should have heroes when HeroListComponent created', () => {
// Pass the mock to the constructor as the Angular injector would
const component = new HeroListComponent(mockService);
expect(component.heroes.length).toEqual(expectedHeroes.length);
});
那些需要其它服务的服务
服务还可以具有自己的依赖。HeroService
非常简单,没有自己的依赖。不过,如果你希望通过日志服务来报告这些活动,那么就可以使用同样的构造函数注入模式,添加一个构造函数来接收一个 Logger
参数。
这是修改后的 HeroService
,它注入了 Logger
,我们把它和前一个版本的服务放在一起进行对比。
- Path:"src/app/heroes/hero.service (v2)" 。
import { Injectable } from '@angular/core';
import { HEROES } from './mock-heroes';
import { Logger } from '../logger.service';
@Injectable({
providedIn: 'root',
})
export class HeroService {
constructor(private logger: Logger) { }
getHeroes() {
this.logger.log('Getting heroes ...');
return HEROES;
}
}
- Path:"src/app/heroes/hero.service (v1)" 。
import { Injectable } from '@angular/core';
import { HEROES } from './mock-heroes';
@Injectable({
providedIn: 'root',
})
export class HeroService {
getHeroes() { return HEROES; }
}
- Path:"src/app/logger.service" 。
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class Logger {
logs: string[] = []; // capture logs for testing
log(message: string) {
this.logs.push(message);
console.log(message);
}
}
该构造函数请求注入一个 Logger
的实例,并把它保存在一个名叫 logger
的私有字段中。 当要求获取英雄列表时,getHeroes()
方法就会记录一条消息。
注意,虽然 Logger
服务没有自己的依赖项,但是它同样带有 @Injectable()
装饰器。实际上,@Injectable()
对所有服务都是必须的。
当 Angular 创建一个构造函数中有参数的类时,它会查找有关这些参数的类型,和供注入使用的元数据,以便找到正确的服务。 如果 Angular 无法找到参数信息,它就会抛出一个错误。 只有当类具有某种装饰器时,Angular 才能找到参数信息。 @Injectable()
装饰器是所有服务类的标准装饰器。
装饰器是 TypeScript 强制要求的。当 TypeScript 把代码转译成 JavaScript 时,一般会丢弃参数的类型信息。只有当类具有装饰器,并且 "tsconfig.json" 中的编译器选项
emitDecoratorMetadata
为true
时,TypeScript 才会保留这些信息。CLI 所配置的 "tsconfig.json" 就带有emitDecoratorMetadata: true
。
这意味着你有责任给所有服务类加上
@Injectable()
。
依赖注入令牌
当使用提供者配置注入器时,就会把提供者和一个 DI
令牌关联起来。 注入器维护一个内部令牌-提供者的映射表,当请求一个依赖项时就会引用它。令牌就是这个映射表的键。
在简单的例子中,依赖项的值是一个实例,而类的类型则充当键来查阅它。 通过把 HeroService
类型作为令牌,你可以直接从注入器中获得一个 HeroService
实例。
Path:"src/app/injector.component.ts" 。
heroService: HeroService;
当你编写的构造函数中需要注入基于类的依赖项时,其行为也类似。 当你使用 HeroService
类的类型来定义构造函数参数时,Angular 就会知道要注入与 HeroService
类这个令牌相关的服务。
Path:"src/app/heroes/hero-list.component.ts" 。
constructor(heroService: HeroService)
很多依赖项的值都是通过类来提供的,但不是全部。扩展的 provide
对象让你可以把多种不同种类的提供者和 DI
令牌关联起来。
可选依赖
HeroService
需要一个记录器,但是如果找不到它会怎么样?
当组件或服务声明某个依赖项时,该类的构造函数会以参数的形式接收那个依赖项。 通过给这个参数加上 @Optional()
注解,你可以告诉 Angular,该依赖是可选的。
import { Optional } from '@angular/core';
constructor(@Optional() private logger?: Logger) {
if (this.logger) {
this.logger.log(some_message);
}
}
当使用 @Optional()
时,你的代码必须能正确处理 null
值。如果你没有在任何地方注册过 logger
提供者,那么注入器就会把 logger
的值设置为 null
。
@Inject()
和@Optional()
都是参数装饰器。它们通过在需要依赖项的类的构造函数上对参数进行注解,来改变 DI 框架提供依赖项的方式。
小结
本节中你学到了 Angular 依赖注入的基础知识。 你可以注册多种提供者,并且知道了如何通过为构造函数添加参数来请求所注入的对象(比如服务)。
更多建议: