Hierarchical Dependency Injectors

多级依赖注入器

You learned the basics of Angular Dependency injection in the Dependency Injection guide.

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

Angular has a Hierarchical Dependency Injection system. There is actually a tree of injectors that parallel an application's component tree. You can reconfigure the injectors at any level of that component tree.

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

This guide explores this system and how to use it to your advantage.

本文将浏览这个体系,并告诉你如何善用它。

Try the.

试试

The injector tree

注入器树

In the Dependency Injection guide, you learned how to configure a dependency injector and how to retrieve dependencies where you need them.

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

In fact, there is no such thing as the injector. An application may have multiple injectors. An Angular application is a tree of components. Each component instance has its own injector. The tree of components parallels the tree of injectors.

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

The component's injector may be a proxy for an ancestor injector higher in the component tree. That's an implementation detail that improves efficiency. You won't notice the difference and your mental model should be that every component has its own injector.

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

Consider this guide's variation on the Tour of Heroes application. At the top is the AppComponent which has some sub-components. One of them is the HeroesListComponent. The HeroesListComponent holds and manages multiple instances of the HeroTaxReturnComponent. The following diagram represents the state of the this guide's three-level component tree when there are three instances of HeroTaxReturnComponent open simultaneously.

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

injector tree

Injector bubbling

注入器冒泡

When a component requests a dependency, Angular tries to satisfy that dependency with a provider registered in that component's own injector. If the component's injector lacks the provider, it passes the request up to its parent component's injector. If that injector can't satisfy the request, it passes it along to its parent injector. The requests keep bubbling up until Angular finds an injector that can handle the request or runs out of ancestor injectors. If it runs out of ancestors, Angular throws an error.

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

You can cap the bubbling. An intermediate component can declare that it is the "host" component. The hunt for providers will climb no higher than the injector for that host component. This is a topic for another day.

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

Re-providing a service at different levels

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

You can re-register a provider for a particular dependency token at multiple levels of the injector tree. You don't have to re-register providers. You shouldn't do so unless you have a good reason. But you can.

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

As the resolution logic works upwards, the first provider encountered wins. Thus, a provider in an intermediate injector intercepts a request for a service from something lower in the tree. It effectively "reconfigures" and "shadows" a provider at a higher level in the tree.

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

If you only specify providers at the top level (typically the root AppModule), the tree of injectors appears to be flat. All requests bubble up to the root NgModule injector that you configured with the bootstrapModule method.

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

Component injectors

组件注入器

The ability to configure one or more providers at different levels opens up interesting and useful possibilities.

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

Scenario: service isolation

场景:服务隔离

Architectural reasons may lead you to restrict access to a service to the application domain where it belongs.

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

The guide sample includes a VillainsListComponent that displays a list of villains. It gets those villains from a VillainsService.

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

While you could provide VillainsService in the root AppModule (that's where you'll find the HeroesService), that would make the VillainsService available everywhere in the application, including the Hero workflows.

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

If you later modified the VillainsService, you could break something in a hero component somewhere. That's not supposed to happen but providing the service in the root AppModule creates that risk.

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

Instead, provide the VillainsService in the providers metadata of the VillainsListComponent like this:

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

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

By providing VillainsService in the VillainsListComponent metadata and nowhere else, the service becomes available only in the VillainsListComponent and its sub-component tree. It's still a singleton, but it's a singleton that exist solely in the villain domain.

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

Now you know that a hero component can't access it. You've reduced your exposure to error.

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

Scenario: multiple edit sessions

场景:多重编辑会话

Many applications allow users to work on several open tasks at the same time. For example, in a tax preparation application, the preparer could be working on several tax returns, switching from one to the other throughout the day.

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

This guide demonstrates that scenario with an example in the Tour of Heroes theme. Imagine an outer HeroListComponent that displays a list of super heroes.

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

To open a hero's tax return, the preparer clicks on a hero name, which opens a component for editing that return. Each selected hero tax return opens in its own component and multiple returns can be open at the same time.

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

Each tax return component has the following characteristics:

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

  • Is its own tax return editing session.

    属于它自己的报税单会话。

  • Can change a tax return without affecting a return in another component.

    可以修改一个报税单,而不会影响另一个组件中的申报单。

  • Has the ability to save the changes to its tax return or cancel them.

    能把所做的修改保存到它的报税单中,或者放弃它们。

Heroes in action

One might suppose that the HeroTaxReturnComponent has logic to manage and restore changes. That would be a pretty easy task for a simple hero tax return. In the real world, with a rich tax return data model, the change management would be tricky. You might delegate that management to a helper service, as this example does.

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

Here is the HeroTaxReturnService. It caches a single HeroTaxReturn, tracks changes to that return, and can save or restore it. It also delegates to the application-wide singleton HeroService, which it gets by injection.

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

import { Injectable } from '@angular/core'; import { HeroTaxReturn } from './hero'; import { HeroesService } from './heroes.service'; @Injectable() export class HeroTaxReturnService { private currentTaxReturn: HeroTaxReturn; private originalTaxReturn: HeroTaxReturn; constructor(private heroService: HeroesService) { } set taxReturn (htr: HeroTaxReturn) { this.originalTaxReturn = htr; this.currentTaxReturn = htr.clone(); } get taxReturn (): HeroTaxReturn { return this.currentTaxReturn; } restoreTaxReturn() { this.taxReturn = this.originalTaxReturn; } saveTaxReturn() { this.taxReturn = this.currentTaxReturn; this.heroService.saveTaxReturn(this.currentTaxReturn).subscribe(); } }

Here is the HeroTaxReturnComponent that makes use of it.

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

import { Component, EventEmitter, Input, Output } from '@angular/core'; import { HeroTaxReturn } from './hero'; import { HeroTaxReturnService } from './hero-tax-return.service'; @Component({ selector: 'app-hero-tax-return', templateUrl: './hero-tax-return.component.html', styleUrls: [ './hero-tax-return.component.css' ], providers: [ HeroTaxReturnService ] }) export class HeroTaxReturnComponent { message = ''; @Output() close = new EventEmitter<void>(); get taxReturn(): HeroTaxReturn { return this.heroTaxReturnService.taxReturn; } @Input() set taxReturn (htr: HeroTaxReturn) { this.heroTaxReturnService.taxReturn = htr; } constructor(private heroTaxReturnService: HeroTaxReturnService ) { } onCanceled() { this.flashMessage('Canceled'); this.heroTaxReturnService.restoreTaxReturn(); }; onClose() { this.close.emit(); }; onSaved() { this.flashMessage('Saved'); this.heroTaxReturnService.saveTaxReturn(); } flashMessage(msg: string) { this.message = msg; setTimeout(() => this.message = '', 500); } }

The tax-return-to-edit arrives via the input property which is implemented with getters and setters. The setter initializes the component's own instance of the HeroTaxReturnService with the incoming return. The getter always returns what that service says is the current state of the hero. The component also asks the service to save and restore this tax return.

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

There'd be big trouble if this service were an application-wide singleton. Every component would share the same service instance. Each component would overwrite the tax return that belonged to another hero. What a mess!

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

Look closely at the metadata for the HeroTaxReturnComponent. Notice the providers property.

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

providers: [ HeroTaxReturnService ]

The HeroTaxReturnComponent has its own provider of the HeroTaxReturnService. Recall that every component instance has its own injector. Providing the service at the component level ensures that every instance of the component gets its own, private instance of the service. No tax return overwriting. No mess.

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

The rest of the scenario code relies on other Angular features and techniques that you can learn about elsewhere in the documentation. You can review it and download it from the.

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

Scenario: specialized providers

场景:专门的提供商

Another reason to re-provide a service is to substitute a more specialized implementation of that service, deeper in the component tree.

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

Consider again the Car example from the Dependency Injection guide. Suppose you configured the root injector (marked as A) with generic providers for CarService, EngineService and TiresService.

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

You create a car component (A) that displays a car constructed from these three generic services.

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

Then you create a child component (B) that defines its own, specialized providers for CarService and EngineService that have special capabilities suitable for whatever is going on in component (B).

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

Component (B) is the parent of another component (C) that defines its own, even more specialized provider for CarService.

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

car components

Behind the scenes, each component sets up its own injector with zero, one, or more providers defined for that component itself.

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

When you resolve an instance of Car at the deepest component (C), its injector produces an instance of Car resolved by injector (C) with an Engine resolved by injector (B) and Tires resolved by the root injector (A).

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

car injector tree

The code for this cars scenario is in the car.components.ts and car.services.ts files of the sample which you can review and download from the.

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