多级依赖注入器

依赖注入一章中,我们已经学过了 Angular 依赖注入的基础知识。

Angular 有一个多级依赖注入系统。 实际上,应用程序中有一个与组件树平行的注入器树(译注:平行是指结构完全相同且一一对应)。 我们可以在组件树中的任何级别上重新配置注入器,达到一些有趣和有用的效果。

在本章中,我们将浏览这个体系,并告诉你如何善用它。

试试在线例子 / 可下载的例子.

注入器树

依赖注入一章中,我们学过如何配置依赖注入器,以及如何在我们需要时用它获取依赖。

实际上,没有那个(唯一的)注入器这回事,一个应用中可能有多个注入器。 一个 Angular 应用是一个组件树。每个组件实例都有自己的注入器! 组件的树与注入器的树平行。

组件的注入器可能是一个组件树中更高级的祖先注入器的代理。 但这只是提升效率的实现细节,我们不用在乎这点差异,在你的脑海里只要想象成每个组件都有自己的注入器就可以了。

考虑《英雄指南》应用的一个简单变种。它的顶层是AppComponent组件,它有一些子组件。 HeroesListComponent组件保存和管理着HeroTaxReturnComponent的多个实例。 下图展示了当HeroesCardComponent的三个 HeroTaxReturnComponent 实例同时展开时的三级组件树状态。

injector tree

注入器冒泡

当一个组件申请获得一个依赖时,Angular 先尝试用该组件自己的注入器来满足它。 如果该组件的注入器没有找到对应的提供商,它就把这个申请转给它父组件的注入器来处理。 如果那个注入器也无法满足这个申请,它就继续转给它的父组件的注入器。 这个申请继续往上冒泡 —— 直到我们找到了一个能处理此申请的注入器或者超出了组件树中的祖先位置为止。 如果超出了组件树中的祖先还未找到,Angular 就会抛出一个错误。

我们还可以“盖住”这次冒泡。一个中层的组件可以声称自己是“宿主”组件。 向上查找提供商的过程会截止于这个“宿主”组件。 我们先保留这个问题,等改天再讨论这个选项。

在不同层级再次提供同一个服务

我们可以在注入器树中的多个层次上为指定的依赖令牌重新注册提供商。 但并非必须重新注册,事实上,虽然可以重新注册,但除非有很好的理由,否则不应该这么做。

服务解析逻辑会自下而上查找,碰到的第一个提供商会胜出。 因此,注入器树中间层注入器上的提供商,可以拦截来自底层的对特定服务的请求。 这导致它可以“重新配置”和者说“遮蔽”高层的注入器。

如果我们只在顶级(通常是根模块AppModule),这三个注入器看起来将是“平面”的。 所有的申请都会冒泡到根NgModule进行处理,也就是我们在bootstrapModule方法中配置的那个。

组件注入器

在不同层次上重新配置一个或多个提供商的能力,开启了一些既有趣又有用的可能性。

场景:服务隔离

出于架构方面的考虑,可能会让你决定把一个服务限制到只能在它所属的特定领域中访问。

本章的范例中包括一个VillainsListComponent,它显示一个反派的列表。

虽然我们也可以在根模块AppModule中提供VillainsService(就像HeroesService那样),不过那样一来就会导致在整个应用中到处都能访问到VillainsService,包括在英雄工作流中。

如果我们以后修改了VillainsService,那就可能会破坏英雄组件中的某些部分。 这可不妙,但是在根模块AppModule中提供这个服务可能会导致这种风险。

我们可以换一种方案:在VillainsListComponent元数据的providers中提供VillainsService,就像这样:

src/app/villains-list.component.ts (metadata)

@Component({
  selector: 'villains-list',
  templateUrl: './villains-list.component.html',
  providers: [ VillainsService ]
})

VillainsListComponent的元数据中而不是其它地方提供VillainsService服务,该服务就会只在VillainsListComponent及其子组件树中可用。 它仍然是单例,但是这个单例只存在于反派(villain)这个领域中。

现在,我们可以确信英雄组件不会访问它,因此减少了犯错误的机会。

场景:多重编辑会话

很多应用允许用户同时进行多个任务。 比如,在纳税申报应用中,申报人可以打开多个报税单,随时可能从一个切换到另一个。

本章要示范的场景仍然是基于《英雄指南》的。 想象一个外层的HeroListComponent,它显示一个超级英雄的列表。

要打开一个英雄的报税单,申报者点击英雄名,它就会打开一个组件来编辑那个申报单。 每个选中的申报单都会在自己的组件中打开,并且可以同时打开多个申报单。

每个报税单组件都有下列特征:

Heroes in action

实现方式之一就是让HeroTaxReturnComponent有逻辑来管理和还原那些更改。 这对于简单的报税单来说是很容易的。 不过,在现实世界中,报税单的数据模型非常复杂,对这些修改的管理可能不得不投机取巧。 于是我们可以把这种管理任务委托给一个辅助服务,就像这个例子中所做的。

这是一个报税单服务HeroTaxReturnService。 它缓存了单条HeroTaxReturn,用于跟踪那个申报单的变更,并且可以保存或还原它。 它还委托给了全应用级的单例服务HeroService,它是通过依赖注入机制取得的。

src/app/hero-tax-return.service.ts

  1. import { Injectable } from '@angular/core';
  2. import { HeroTaxReturn } from './hero';
  3. import { HeroesService } from './heroes.service';
  4. @Injectable()
  5. export class HeroTaxReturnService {
  6. private currentTaxReturn: HeroTaxReturn;
  7. private originalTaxReturn: HeroTaxReturn;
  8. constructor(private heroService: HeroesService) { }
  9. set taxReturn (htr: HeroTaxReturn) {
  10. this.originalTaxReturn = htr;
  11. this.currentTaxReturn = htr.clone();
  12. }
  13. get taxReturn (): HeroTaxReturn {
  14. return this.currentTaxReturn;
  15. }
  16. restoreTaxReturn() {
  17. this.taxReturn = this.originalTaxReturn;
  18. }
  19. saveTaxReturn() {
  20. this.taxReturn = this.currentTaxReturn;
  21. this.heroService.saveTaxReturn(this.currentTaxReturn).subscribe();
  22. }
  23. }

下面是正在使用它的HeroTaxReturnComponent组件。

src/app/hero-tax-return.component.ts

  1. import { Component, EventEmitter, Input, Output } from '@angular/core';
  2. import { HeroTaxReturn } from './hero';
  3. import { HeroTaxReturnService } from './hero-tax-return.service';
  4. @Component({
  5. selector: 'hero-tax-return',
  6. templateUrl: './hero-tax-return.component.html',
  7. styleUrls: [ './hero-tax-return.component.css' ],
  8. providers: [ HeroTaxReturnService ]
  9. })
  10. export class HeroTaxReturnComponent {
  11. message = '';
  12. @Output() close = new EventEmitter<void>();
  13. get taxReturn(): HeroTaxReturn {
  14. return this.heroTaxReturnService.taxReturn;
  15. }
  16. @Input()
  17. set taxReturn (htr: HeroTaxReturn) {
  18. this.heroTaxReturnService.taxReturn = htr;
  19. }
  20. constructor(private heroTaxReturnService: HeroTaxReturnService ) { }
  21. onCanceled() {
  22. this.flashMessage('Canceled');
  23. this.heroTaxReturnService.restoreTaxReturn();
  24. };
  25. onClose() { this.close.emit(); };
  26. onSaved() {
  27. this.flashMessage('Saved');
  28. this.heroTaxReturnService.saveTaxReturn();
  29. }
  30. flashMessage(msg: string) {
  31. this.message = msg;
  32. setTimeout(() => this.message = '', 500);
  33. }
  34. }

我们通过输入属性得到要编辑的报税单,我们把它实现成了读取器(getter)和设置器(setter)。 设置器根据传进来的报税单初始化了组件自己的HeroTaxReturnService实例。 读取器总是返回该服务所存英雄的当前状态。 组件也会请求该服务来保存或还原这个报税单。

这里有个大问题,那就是如果这个服务是一个全应用范围的单例,每个组件就都会共享同一个服务实例,每个组件也都会覆盖属于其他英雄的报税单,真是一团糟!

但仔细看HeroTaxReturnComponent的元数据,注意providers属性。

providers: [ HeroTaxReturnService ]

HeroTaxReturnComponent有它自己的HeroTaxReturnService提供商。 回忆一下,每个组件的实例都有它自己的注入器。 在组件级提供服务可以确保组件的每个实例都得到一个自己的、私有的服务实例。 报税单不会再被意外覆盖,这下清楚了。

该场景代码中的其它部分依赖另一些Angular的特性和技术,我们将会在本文档的其它章节学到。 你可以到在线例子 / 可下载的例子查看代码和下载它。

场景:专门的提供商

重新提供服务的另一个原因,是在组件树的深层中把该服务替换为一个更特殊的实现。

再次考虑依赖注入一章中车辆(Car)的例子。 假设我们在根注入器(代号A)中配置了通用的提供商:CarServiceEngineServiceTiresService

我们创建了一个车辆组件(A),它显示一个从另外三个通用服务构造出的车辆。

然后,我们创建一个子组件(B),它为CarServiceEngineService定义了自己的特殊的提供商,它们具有更特殊的能力,适用于组件B的。

组件B是另一个组件C的父组件,而组件C又定义了自己的,更特殊的CarService提供商。

car components

在幕后,每个组件都有自己的注入器,这个注入器带有为组件本身准备的0个、1个或多个提供商。

当我们在最深层的组件C解析Car的实例时,它使用注入器C解析生成了一个Car的实例,使用注入器B解析了Engine,而Tires则是由根注入器A解析的。

car injector tree

车辆场景下的代码位于car.components.tscar.services.ts文件中,这个例子你可以在在线例子 / 可下载的例子查看和下载。