Angular 中的依赖注入link
依赖注入(DI)是一种重要的应用设计模式。 Angular 有自己的 DI 框架,在设计应用时常会用到它,以提升它们的开发效率和模块化程度。
依赖,是当类需要执行其功能时,所需要的服务或对象。 DI 是一种编码模式,其中的类会从外部源中请求获取依赖,而不是自己创建它们。
在 Angular 中,DI 框架会在实例化该类时向其提供这个类所声明的依赖项。本指南介绍了 DI 在 Angular 中的工作原理,以及如何借助它来让你的应用更灵活、高效、健壮,以及可测试、可维护。
我们先看一下英雄指南中英雄管理特性的简化版。这个简化版不使用 DI,我们将逐步把它转换成使用 DI 的。
src/app/heroes/heroes.component.ts
src/app/heroes/hero-list.component.ts
src/app/heroes/hero.ts
src/app/heroes/mock-heroes.ts
content_copyimport { 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
文件中定义了一个内存集合)中获取英雄。
src/app/heroes/hero-list.component.ts (class)
content_copyexport class HeroListComponent {
heroes = HEROES;
}
这种方法在原型阶段有用,但是不够健壮、不利于维护。 一旦你想要测试该组件或想从远程服务器获得英雄列表,就不得不修改 HeroesListComponent
的实现,并且替换每一处使用了 HEROES
模拟数据的地方。
创建和注册可注入的服务
DI 框架让你能从一个可注入的服务类(独立文件)中为组件提供数据。为了演示,我们还会创建一个用来提供英雄列表的、可注入的服务类,并把它注册为该服务的提供商。
在同一个文件中放多个类容易让人困惑。我们通常建议你在单独的文件中定义组件和服务。
如果你把组件和服务都放在同一个文件中,请务必先定义服务,然后再定义组件。如果在服务之前定义组件,则会在运行时收到一个空引用错误。
也可以借助 forwardRef()
方法来先定义组件,就像这个博客中解释的那样。
你还可以使用前向引用来打破循环依赖,参见 DI 一章中的例子。
创建可注入的服务类
Angular CLI 可以用下列命令在 src/app/heroes
目录下生成一个新的 HeroService
类。
content_copyng generate service heroes/hero
下列命令会创建 HeroService
的骨架。
src/app/heroes/hero.service.ts (CLI-generated)
content_copyimport { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class HeroService {
constructor() { }
}
@Injectable()
是每个 Angular 服务定义中的基本要素。该类的其余部分导出了一个 getHeroes
方法,它会返回像以前一样的模拟数据。(真实的应用可能会从远程服务器中异步获取这些数据,不过这里我们先忽略它,专心实现服务的注入机制。)
src/app/heroes/hero.service.ts
content_copyimport { 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
。
src/app/heroes/hero-list.component (constructor signature)
content_copyconstructor(heroService: HeroService)
当然,HeroListComponent
还应该使用注入的这个 HeroService
做一些事情。 这里是修改过的组件,它转而使用注入的服务。与前一版本并列显示,以便比较。
hero-list.component (with DI)
hero-list.component (without DI)
content_copyimport { 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();
}
}
必须在某些父注入器中提供 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
。
src/app/test.component.ts
content_copyconst 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
,我们把它和前一个版本的服务放在一起进行对比。
src/app/heroes/hero.service (v2)
src/app/heroes/hero.service (v1)
src/app/logger.service
content_copyimport { 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;
}
}
该构造函数请求注入一个 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
实例。
src/app/injector.component.ts
content_copyheroService: HeroService;
当你编写的构造函数中需要注入基于类的依赖项时,其行为也类似。 当你使用 HeroService
类的类型来定义构造函数参数时,Angular 就会知道要注入与 HeroService
类这个令牌相关的服务。
src/app/heroes/hero-list.component.ts
content_copyconstructor(heroService: HeroService)
很多依赖项的值都是通过类来提供的,但不是全部。扩展的 provide 对象让你可以把多种不同种类的提供商和 DI 令牌关联起来。
-
欲知详情,参见不同种类的提供商。
可选依赖
HeroService
需要一个记录器,但是如果找不到它会怎么样?
当组件或服务声明某个依赖项时,该类的构造函数会以参数的形式接收那个依赖项。 通过给这个参数加上 @Optional()
注解,你可以告诉 Angular,该依赖是可选的。
content_copyimport { Optional } from '@angular/core';
content_copyconstructor(@Optional() private logger: Logger) {
if (this.logger) {
this.logger.log(some_message);
}
}
当使用 @Optional()
时,你的代码必须能正确处理 null 值。如果你没有在任何地方注册过 logger 提供商,那么注入器就会把 logger
的值设置为 null。
@Inject()
和 @Optional()
都是参数装饰器。它们通过在需要依赖项的类的构造函数上对参数进行注解,来改变 DI 框架提供依赖项的方式。
欲知详情,参见多级注入器。
小结
本页中你学到了 Angular 依赖注入的基础知识。 你可以注册多种提供商,并且知道了如何通过为构造函数添加参数来请求所注入的对象(比如服务)。
在以下页面中可以深入了解 Angular DI 体系的能力及高级特性: