官方文档
Angular 中有两个注入器层次结构:
(1) ModuleInjector 层次结构 —— 使用 @NgModule() 或 @Injectable() 注解在此层次结构中配置 ModuleInjector。
(2) ElementInjector 层次结构 —— 在每个 DOM 元素上隐式创建。除非你在 @Directive() 或 @Component() 的 providers 属性中进行配置,否则默认情况下,ElementInjector 为空。
意思是,只要我们在 @NgModule 里通过 providers 数组定义服务提供者,以及在服务实现类里使用 @Injectable 注解,我们实际上就在定义 ModuleInjector.
ModuleInjector
可以通过以下两种方式之一配置 ModuleInjector :
- 使用 @Injectable() 的 providedIn 属性引用 @NgModule() 或 root。
- 使用 @NgModule() 的 providers 数组。
使用 @Injectable() 的 providedIn 属性优于 @NgModule() 的 providers 数组,因为使用 @Injectable() 的 providedIn 时,优化工具可以进行 tree shaking,从而删除你的应用程序中未使用的服务,以减小捆绑包尺寸。
下面是通过 NgModule.providers 定义 ModuleInjector 注释:
@usageNotes — Dependencies whose providers are listed here become available for injection into any component, directive, pipe or service that is a child of this injector. The NgModule used for bootstrapping uses the root injector, and can provide dependencies to any part of the app.
A lazy-loaded module has its own injector, typically a child of the app root injector. Lazy-loaded services are scoped to the lazy-loaded module's injector. If a lazy-loaded module also provides the UserService, any component created within that module's context (such as by router navigation) gets the local instance of the service, not the instance in the root injector. Components in external modules continue to receive the instance provided by their injectors.
子 ModuleInjector 是在惰性加载其它 @NgModules 时创建的。
使用 @Injectable() 的 providedIn 属性提供服务的方式如下:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root' // <--provides this service in the root ModuleInjector
})
export class ItemService {
name = 'telephone';
}
@Injectable() 装饰器标识服务类。该 providedIn 属性配置指定的 ModuleInjector,这里的 root 会把让该服务在 root ModuleInjector 上可用。
平台注入器
在 root 之上还有两个注入器,一个是额外的 ModuleInjector,一个是 NullInjector()。
思考下 Angular 要如何通过 main.ts 中的如下代码引导应用程序:
platformBrowserDynamic().bootstrapModule(AppModule).then(ref => {...})
bootstrapModule() 方法会创建一个由 AppModule 配置的注入器作为平台注入器的子注入器。也就是 root ModuleInjector。
platformBrowserDynamic() 方法创建一个由 PlatformModule 配置的注入器,该注入器包含特定平台的依赖项。这允许多个应用共享同一套平台配置。例如,无论你运行多少个应用程序,浏览器都只有一个 URL 栏。你可以使用 platformBrowser() 函数提供 extraProviders,从而在平台级别配置特定平台的额外提供者。
层次结构中的下一个父注入器是 NullInjector(),它是树的顶部。如果你在树中向上走了很远,以至于要在 NullInjector() 中寻找服务,那么除非使用 @Optional(),否则将收到错误消息,因为最终所有东西都将以 NullInjector() 结束并返回错误,或者对于 @Optional(),返回 null。
NullInjector 相当于注入器机制的错误处理,default 机制。
下图展示了前面各段落描述的 root ModuleInjector 及其父注入器之间的关系。
ElementInjector
Angular 会为每个 DOM 元素隐式创建 ElementInjector。
可以用 @Component() 装饰器中的 providers 或 viewProviders 属性来配置 ElementInjector 以提供服务。例如,下面的 TestComponent 通过提供此服务来配置 ElementInjector:
@Component({
...
providers: [{ provide: ItemService, useValue: { name: 'lamp' } }]
})
export class TestComponent
这地方有点费解,关 DOM 什么事?
@Directive() 和 @Component()
组件是一种特殊类型的指令,这意味着 @Directive() 具有 providers 属性,@Component() 也同样如此。 这意味着指令和组件都可以使用 providers 属性来配置提供者。当使用 providers 属性为组件或指令配置提供者时,该提供程商就属于该组件或指令的 ElementInjector。同一元素上的组件和指令共享同一个注入器。
解析规则
当为组件/指令解析令牌时,Angular 分为两个阶段来解析它:
- 针对 ElementInjector 层次结构(其父级)
- 针对 ModuleInjector 层次结构(其父级)
当组件声明依赖项时,Angular 会尝试使用它自己的 ElementInjector 来满足该依赖。 如果组件的注入器缺少提供者,它将把请求传给其父组件的 ElementInjector。
这些请求将继续转发,直到 Angular 找到可以处理该请求的注入器或用完祖先 ElementInjector。
如果 Angular 在任何 ElementInjector 中都找不到提供者,它将返回到发起请求的元素,并在 ModuleInjector 层次结构中进行查找。如果 Angular 仍然找不到提供者,它将引发错误。
如果你已在不同级别注册了相同 DI 令牌的提供者,则 Angular 会用遇到的第一个来解析该依赖。例如,如果提供者已经在需要此服务的组件中本地注册了,则 Angular 不会再寻找同一服务的其它提供者。
解析修饰符
解析修饰符分为三类:
- 如果 Angular 找不到你要的东西该怎么办,用 @Optional()
- 从哪里开始寻找,用 @SkipSelf()
- 到哪里停止寻找,用 @Host() 和 @Self()
默认情况下,Angular 始终从当前的 Injector 开始,并一直向上搜索。修饰符使你可以更改开始(默认是自己)或结束位置。
@Optional()
@Optional() 允许 Angular 将你注入的服务视为可选服务。这样,如果无法在运行时解析它,Angular 只会将服务解析为 null,而不会抛出错误。在下面的范例中,服务 OptionalService 没有在 @NgModule() 或组件类中提供,所以它没有在应用中的任何地方。
export class OptionalComponent {
constructor(@Optional() public optional?: OptionalService) {}
}
@Self()
使用 @Self() 让 Angular 仅查看当前组件或指令的 ElementInjector。
@Self() 的一个好例子是要注入某个服务,但只有当该服务在当前宿主元素上可用时才行。为了避免这种情况下出错,请将 @Self() 与 @Optional() 结合使用。
ElementInjector 用例范例
场景:服务隔离
出于架构方面的考虑,可能会让你决定把一个服务限制到只能在它所属的那个应用域中访问。 比如,这个例子中包括一个用于显示反派列表的 VillainsListComponent,它会从 VillainsService 中获得反派列表数据。
如果你在根模块 AppModule 中(也就是你注册 HeroesService 的地方)提供 VillainsService,就会让应用中的任何地方都能访问到 VillainsService,包括针对英雄的工作流。如果你稍后修改了 VillainsService,就可能破坏了英雄组件中的某些地方。在根模块 AppModule 中提供该服务将会引入此风险。
该怎么做呢?你可以在 VillainsListComponent 的 providers 元数据中提供 VillainsService,就像这样:
@Component({
selector: 'app-villains-list',
templateUrl: './villains-list.component.html',
providers: [ VillainsService ]
})
在 VillainsListComponent 的元数据中而不是其它地方提供 VillainsService 服务,该服务就会只在 VillainsListComponent 及其子组件树中可用。
VillainService 对于 VillainsListComponent 来说是单例的,因为它就是在这里声明的。只要 VillainsListComponent 没有销毁,它就始终是 VillainService 的同一个实例。但是对于 VillainsListComponent 的多个实例,每个 VillainsListComponent 的实例都会有自己的 VillainService 实例。
每个组件的实例都有它自己的注入器。 在组件级提供服务可以确保组件的每个实例都得到一个自己的、私有的服务实例。
场景:专门的提供者
这就是 SAP Spartacus 典型的应用场景。
在其它层级重新提供服务的另一个理由,是在组件树的深层中把该服务替换为一个更专门化的实现。
代码第13行的 BudgetCostCenterListService 是 ListService 的一个具体实现。
更多Jerry的原创文章,尽在:"汪子熙":