依赖注入
@Injectable() 装饰器把一个类定义为 Angular 中的服务,并且允许 Angular 把它作为依赖注入到组件中。 类似的,@Injectable() 装饰器会标记出某个组件、类、管道或 NgModule 具有对某个服务的依赖。
- Angular 会在启动过程中为你创建全应用级注入器以及所需的其它注入器。你不用自己创建注入器。
- 该注入器会创建依赖、维护一个容器来管理这些依赖,并尽可能复用它们。
- 提供者(provider)是一个对象,用来告诉注入器应该如何获取或创建依赖
你的应用中所需的任何依赖,都必须使用该应用的注入器来注册一个提供者,以便注入器可以使用这个提供者来创建新实例。对于服务,该提供者通常就是服务类本身。
依赖不一定是服务 —— 它还可能是函数或值。
当 Angular 创建组件类的新实例时,它会通过查看该组件类的构造函数,来决定该组件依赖哪些服务或其它依赖项。比如 HeroListComponent 的构造函数中需要 HeroService:
1 | constructor(private service: HeroService) { } |
当 Angular 发现某个组件依赖某个服务时,它会首先检查是否该注入器中已经有了那个服务的任何现有实例。如果所请求的服务尚不存在,注入器就会使用以前注册的服务提供者来制作一个,并把它加入注入器中,然后把该服务返回给 Angular。
Angular 如何知道 class 有哪些依赖?
JS 没有反射,那 Angular 怎么能从 ServiceB 的 constructor 感知到其依赖 ServiceA 呢?
1 | class ServiceB { |
答案是黑魔法 Compilation。 ServiceB 经过 compile 后会变成这样
它多了一个 ɵfac 静态方法。
从代码上可以推测出 injector.get(ServiceB),其实并不是直接执行了 new ServiceB(new ServiceA()),它只是调用了 ServiceB.ɵfac()。
而 ɵfac 内容才是 new ServiceB( inject(ServiceA) )。这句代码便是 compiler 透过反射 constructor 得知 ServiceB 依赖 ServiceA 后写出来的。
另外,inject(ServiceA) 是一个递归实例化依赖函数,里面一定是调用了 ServiceA.ɵfac()。以此类推,一直到所有的依赖全部被实例化。
简而言之,虽然 JS 没有反射,但是 Angular compiler 可以反射,然后自动编写出实例化依赖的代码。这就是 Angular DI 的实现秘诀啦。
提供服务范围
对于要用到的任何服务,你必须至少注册一个提供者。服务可以在自己的元数据中把自己注册为提供者,这样可以让自己随处可用。或者,你也可以为特定的模块或组件注册提供者。要注册提供者,就要在服务的 @Injectable() 装饰器中提供它的元数据,或者在 @NgModule() 或 @Component() 的元数据中。
- 默认情况下,Angular CLI 的 ng generate service 命令会在 @Injectable() 装饰器中提供元数据来把它注册到根注入器中。
1 | ({ |
当你在根一级提供服务时,Angular 会为 HeroService 创建一个单一的共享实例,并且把它注入到任何想要它的类中。
这种在 @Injectable 元数据中注册提供者的方式还让 Angular 能够通过移除那些从未被用过的服务来优化大小,这个过程称为摇树优化(tree-shaking)。
- 当你使用特定的 NgModule 注册提供者时,该服务的同一个实例将会对该 NgModule 中的所有组件可用。要想在这一层注册,请用 @NgModule() 装饰器中的 providers 属性:
1 | ({ |
- 当你在组件级注册提供者时,你会为该组件的每一个新实例提供该服务的一个新实例。要在组件级注册,就要在 @Component() 元数据的 providers 属性中注册服务提供者。
1
2
3
4
5({
selector: 'app-hero-list',
templateUrl: './hero-list.component.html',
providers: [ HeroService ]
})
抽象理解 provider 和 injector
provider 的特性
抽象的看,provider 是一个 key value pair 对象。
key 的作用是为了识别。
value 则是一个提供最终值的 factory 函数。
只要能满足这 2 点,那它就可以被作为 provider。
Injector 的特性
Injector 不仅仅是实例化机器。
抽象的看,injector 第一个任务是通过 key 查找出指定的 provider,这个 key 只要具备可识别性就可以了。比如:string,class,symbol 等等都具备识别性。
第二个任务是通过 provider value factory 生产出最终的值。当然如果这个 factory 需要依赖,injector 会先查找它所需要的依赖,注入给 factory 函数。
Provider & StaticProvider
Injector.create 的 interface 长这样
Angular 有多种不同形态的 Provider.
Provider 和 StaticProvider 是所有 Provider 的抽象。
1 | type Provider = TypeProvider | ValueProvider | ClassProvider | ConstructorProvider | ExistingProvider | FactoryProvider | any[]; |
它俩是有重叠的,总的来说是 TypeProvider、ClassProvider、StaticClassProvider、ConstructorProvider、FactoryProvider、ValueProvider、ExistingProvider。
配置依赖提供者
你还可以用其他值作为依赖项,例如 Boolean、字符串、日期和对象。 Angular DI 提供了一些必要的 API 来让依赖的配置方式更加灵活,以便你可以把这些值在 DI 中可用。
指定提供者令牌
如果你用服务类作为提供者令牌,则其默认行为是注入器使用 new 运算符实例化该类。
在下面这个例子中,Logger 类提供了 Logger 的实例。
1 | providers: [Logger] |
但是,你可以将 DI 配置为使用不同的类或任何其他不同的值来与 Logger 类关联。因此,当注入 Logger 时,会改为使用这个新值。
实际上,类提供者语法是一个简写表达式,可以扩展为由 Provider 接口定义的提供者配置信息。
在这种情况下,Angular 将 providers 值展开为完整的提供者对象,如下所示:
1 | [{ provide: Logger, useClass: Logger }] |
展开后的提供者配置是一个具有两个属性的对象字面量:
provide 属性包含一个令牌,该令牌会作为定位依赖值和配置注入器时的键。
第二个属性是一个提供者定义对象,它会告诉注入器如何创建依赖值。 提供者定义对象中的键可以是以下值之一:
useClass -此选项告诉 Angular DI 在注入依赖项时要实例化这里提供的类
useExisting - 允许你为令牌起一个别名,并引用任意一个现有令牌。
useFactory - 允许你定义一个用来构造依赖项的函数
useValue - 提供了一个应该作为依赖项使用的静态值。
下面的部分介绍如何使用这里所说的“提供者定义”键。
类提供者:useClass
useClass 能让你创建并返回指定类的新实例。
你可以用这种类型的提供者来作为通用类或默认类的替代实现。
例如,替代实现可以实现不同的策略、扩展默认类或模拟测试用例中真实类的行为。在以下示例中,当在组件或任何其他类中请求 Logger 依赖项时,将转而实例化 BetterLogger 类。
1 | [{ provide: Logger, useClass: BetterLogger }] |
如果替代类提供者有自己的依赖项,请在父模块或组件的 providers 元数据属性中指定这两个提供者。
1 | [ UserService, |
在这个例子中,EvenBetterLogger 会在日志信息里显示用户名。这个 logger 要从注入的 UserService 实例中来获取该用户。
1 | () |
别名提供者:useExisting
useExisting 允许你将一个令牌映射到另一个。实际上,第一个令牌是与第二个令牌关联的服务的别名,创建了两种访问同一个服务对象的方式。
在下面的例子中,当组件请求新的或旧的记录器时,注入器都会注入一个 NewLogger 的实例。通过这种方式,OldLogger 就成了 NewLogger 的别名。
1 | [ NewLogger, |
确保你没有使用 OldLogger 将 NewLogger 别名为 useClass ,因为这会创建两个不同 NewLogger 实例。
工厂提供者:useFactory
useFactory 允许你通过调用工厂函数来创建依赖对象。使用这种方法,你可以根据 DI 和应用程序中其他地方的可用信息创建动态值。
在下面的例子中,只有授权用户才能看到 HeroService 中的秘密英雄。授权可能在单个应用会话期间发生变化,比如改用其他用户登录。
要想在 UserService 和 HeroService 中保存敏感信息,就要给 HeroService 的构造函数传一个逻辑标志来控制秘密英雄的显示。
hero.service.ts
1
2
3
4
5
6
7
8
9 constructor(
private logger: Logger,
private isAuthorized: boolean) { }
getHeroes() {
const auth = this.isAuthorized ? 'authorized > ' : 'unauthorized';
this.logger.log(`Getting heroes for ${auth} user.`);
return HEROES.filter(hero => this.isAuthorized || !hero.isSecret);
}要实现 isAuthorized 标志,可以用工厂提供者来为 HeroService 创建一个新的 logger 实例。
1 | const heroServiceFactory = (logger: Logger, userService: UserService) => |
这个工厂函数可以访问 UserService。你可以同时把 Logger 和 UserService 注入到工厂提供者中,这样注入器就可以把它们传给工厂函数了。
1 | export const heroServiceProvider = |
useFactory 字段指定该提供者是一个工厂函数,其实现代码是 heroServiceFactory。
deps 属性是一个提供者令牌的数组。 Logger 和 UserService 类作为它们自己的类提供者的令牌。注入器会解析这些令牌,并将相应的服务注入到匹配的 heroServiceFactory 工厂函数参数中。
通过把工厂提供者导出为变量 heroServiceProvider,就能让工厂提供者变得可复用。
值提供者:useValue
useValue 允许你将固定值与某个 DI 令牌相关联。可以用此技术提供运行时配置常量,例如网站基址和特性标志。你还可以在单元测试中使用值提供者来提供模拟数据以代替生产级数据服务。
使用 InjectionToken 对象
可以定义和使用一个 InjectionToken 对象来为非类的依赖选择一个提供者令牌。下列例子定义了一个类型为 InjectionToken 的 APP_CONFIG。
1 | import { InjectionToken } from '@angular/core'; |
可选的参数
接着,用 APP_CONFIG 这个 InjectionToken 对象在组件中注册依赖提供者。
1 | providers: [{ provide: APP_CONFIG, useValue: HERO_DI_CONFIG }] |
现在,借助参数装饰器 @Inject(),你可以把这个配置对象注入到构造函数中。
1 | constructor() { (APP_CONFIG) config: AppConfig |
使用参数装饰器来限定依赖查找方式
默认情况下,DI 框架会在注入器树中查找一个提供者,从该组件的局部注入器开始,如果需要,则沿着注入器树向上冒泡,直到根注入器。
第一个配置过该提供者的注入器就会把依赖(服务实例或值)提供给这个构造函数
如果在根注入器中也没有找到提供者,则 DI 框架将会抛出一个错误
通过在类的构造函数中对服务参数使用参数装饰器,可以提供一些选项来修改默认的搜索行为。
用 @Optional 来让依赖是可选的,以及使用 @Host 来限定搜索方式
某些情况下,你需要限制搜索,或容忍依赖项的缺失。你可以使用组件构造函数参数上的 @Host 和 @Optional 这两个限定装饰器来修改 Angular 的搜索行为。
@Optional 属性装饰器告诉 Angular 当找不到依赖时就返回 null
@Host 属性装饰器会禁止在宿主组件以上的搜索。宿主组件通常就是请求该依赖的那个组件。不过,当该组件投影进某个父组件时,那个父组件就会变成宿主。
1 | template: ` |
在
1
2
3
4template: `
<h4>{{hero.name}}</h4>
<ng-content></ng-content>
<textarea cols="25" [(ngModel)]="hero.description"></textarea>`,
使用 @Inject 指定自定义提供者
自定义提供者让你可以为隐式依赖提供一个具体的实现,比如内置浏览器 API。下面的例子使用 InjectionToken 来提供 localStorage,将其作为 BrowserStorageService 的依赖项。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29 import { Inject, Injectable, InjectionToken } from '@angular/core';
export const BROWSER_STORAGE = new InjectionToken<Storage>('Browser Storage', {
providedIn: 'root',
factory: () => localStorage
});
({
providedIn: 'root'
})
export class BrowserStorageService {
constructor(public storage: Storage) {} (BROWSER_STORAGE)
get(key: string) {
return this.storage.getItem(key);
}
set(key: string, value: string) {
this.storage.setItem(key, value);
}
remove(key: string) {
this.storage.removeItem(key);
}
clear() {
this.storage.clear();
}
}
factory 函数返回 window 对象上的 localStorage 属性。Inject 装饰器修饰一个构造函数参数,用于为某个依赖提供自定义提供者。
使用 @Self 和 @SkipSelf 来修改提供者的搜索方式
注入器也可以通过构造函数的参数装饰器来指定范围。下面的例子就在 Component 类的 providers 中使用浏览器的 sessionStorage API 覆盖了 BROWSER_STORAGE 令牌。同一个 BrowserStorageService 在构造函数中使用 @Self 和 @SkipSelf 装饰器注入了两次,来分别指定由哪个注入器来提供依赖。
1 | import { Component, OnInit, Self, SkipSelf } from '@angular/core'; |
使用 @Self 装饰器时,注入器只在该组件的注入器中查找提供者。@SkipSelf 装饰器可以让你跳过局部注入器,并在注入器树中向上查找,以发现哪个提供者满足该依赖。sessionStorageService 实例使用浏览器的 sessionStorage 来跟 BrowserStorageService 打交道,而 localStorageService 跳过了局部注入器,使用根注入器提供的 BrowserStorageService,它使用浏览器的 localStorage API。
注入器层次结构的类型
Angular 中有两个注入器层次结构:
注入器层次结构 | 详情 |
---|---|
ModuleInjector 层次结构 | 使用 @NgModule() 或 @Injectable() 注解在此层次结构中配置 ModuleInjector。 |
ElementInjector 层次结构 | 在每个 DOM 元素上隐式创建。默认情况下,ElementInjector 是空的,除非你在 @Directive() 或 @Component() 的 providers 属性中配置它。 |
ModuleInjector
可以通过以下两种方式之一配置 ModuleInjector :
使用 @Injectable() 的 providedIn 属性引用 @NgModule() 或 root
使用 @NgModule() 的 providers 数组
摇树优化与 @Injectable()
使用 @Injectable() 的 providedIn 属性优于 @NgModule() 的 providers 数组。使用 @Injectable() 的 providedIn 时,优化工具可以进行摇树优化,从而删除你的应用程序中未使用的服务,以减小捆绑包尺寸。
平台注入器
在 root 之上还有两个注入器,一个是额外的 ModuleInjector,一个是 NullInjector()。
思考下 Angular 要如何通过 main.ts 中的如下代码引导应用程序:
1 | platformBrowserDynamic().bootstrapModule(AppModule).then(ref => {…}) |
bootstrapModule() 方法会创建一个由 AppModule 配置的注入器作为平台注入器的子注入器。也就是 root ModuleInjector。
platformBrowserDynamic() 方法创建一个由 PlatformModule 配置的注入器,该注入器包含特定平台的依赖项。这允许多个应用共享同一套平台配置。比如,无论你运行多少个应用程序,浏览器都只有一个 URL 栏。你可以使用 platformBrowser() 函数提供 extraProviders,从而在平台级别配置特定平台的额外提供者。
层次结构中的下一个父注入器是 NullInjector(),它是树的顶部。如果你在树中向上走了很远,以至于要在 NullInjector() 中寻找服务,那么除非使用 @Optional(),否则将收到错误消息,因为最终所有东西都将以 NullInjector() 结束并返回错误,或者对于 @Optional(),返回 null。
NullInjector()
(always throws an error unless you use @Optional)
↓
ModuleInjector
(configured by PlatformModule)
has special things like DomSanitizer=>platformBrowser()↓
root ModuleInjector
(configured by YourAppModule)
has things for your app=>bootstrapModule(YourAPPModule)
虽然 root 是一个特殊的别名,但其它 ModuleInjector 都没有别名。每当创建动态加载组件时,你还会创建 ModuleInjector,比如路由器,它还会创建子 ModuleInjector。
无论是使用 bootstrapModule() 的方法配置它,还是将所有提供者都用 root 注册到其自己的服务中,所有请求最终都会转发到 root 注入器。
如果你在 AppModule 的 @NgModule() 中配置应用级提供者,它就会覆盖一个在 @Injectable() 的 root 元数据中配置的提供者。你可以用这种方式,来配置供多个应用共享的服务的非默认提供者。
ElementInjector
Angular 会为每个 DOM 元素隐式创建 ElementInjector。
可以用 @Component() 装饰器中的 providers 或 viewProviders 属性来配置 ElementInjector 以提供服务。比如,下面的 TestComponent 通过提供此服务来配置 ElementInjector
1 | ({ |
@Directive() 和 @Component()
组件是一种特殊类型的指令,这意味着 @Directive() 具有 providers 属性,@Component() 也同样如此。 这意味着指令和组件都可以使用 providers 属性来配置提供者。当使用 providers 属性为组件或指令配置提供者时,该提供程商就属于该组件或指令的 ElementInjector。同一元素上的组件和指令共享同一个注入器。
解析规则
当为组件/指令解析令牌时,Angular 分为两个阶段来解析它:
针对 ElementInjector 层次结构中它的父级。
针对 ModuleInjector 层次结构中它的父级。
当组件声明依赖项时,Angular 会尝试使用它自己的 ElementInjector 来满足该依赖。 如果组件的注入器缺少提供者,它将把请求传给其父组件的 ElementInjector。
这些请求将继续转发,直到 Angular 找到可以处理该请求的注入器或用完祖先 ElementInjector。
如果 Angular 在任何 ElementInjector 中都找不到提供者,它将返回到发起请求的元素,并在 ModuleInjector 层次结构中进行查找。如果 Angular 仍然找不到提供者,它将引发错误。
如果你已在不同级别注册了相同 DI 令牌的提供者,则 Angular 会用遇到的第一个来解析该依赖。比如,如果提供者已经在需要此服务的组件中本地注册了,则 Angular 不会再寻找同一服务的其它提供者。
模板的逻辑结构
1 | <app-root> |
下面是如何将
1 | <app-root> |
当你在组件类中配置服务时,了解这种 <#VIEW> 划界的思想尤其重要。
使用 viewProviders 数组
使用 viewProviders 数组是在 @Component() 装饰器中提供服务的另一种方法。使用 viewProviders 使服务在 <#VIEW> 中可见。
除了使用 viewProviders 数组外,其它步骤与使用 providers 数组相同。
1 | <app-root @NgModule(AppModule) |
这四个绑定说明了 providers 和 viewProviders 之间的区别。由于🐶(小狗)在 <#VIEW> 中声明,因此投影内容不可见。投影的内容中会看到🐳(鲸鱼)。
但是下一部分,InspectorComponent 是 ChildComponent 的子组件,InspectorComponent 在 <#VIEW> 内部,因此当它请求 AnimalService 时,它会看到🐶(小狗)。
@Host() 和 viewProviders
@Host() 将搜索的上限限制为父节点的 <#VIEW>
1 | <app-root @NgModule(AppModule) |
@SkipSelf() 导致注入器从
@Inject()、Provider.deps、inject
@Inject()
@inject 主要的使用场景是在 class constructor 注入 token。
1 | const VALUE_TOKEN = new InjectionToken<string>('Value'); |
注: 要搭配 @Injectable 指令
inject 函数可以完全取代 @Inject(),上面代码可以改成这样
1 | class ServiceA { |
连 @Injectable() 也可以省略掉
Provider.deps
除了 @Inject(),还有一种注入方式是通过 Provider.deps
1 | const VALUE_1_TOKEN = new InjectionToken<string>('Value1'); |
这个同样可以被 inject 函数替代。
1 | providers: [ |
inject 函数
显然,inject 函数就是用来替代 @Inject 和 Provider.deps 的,所以尽量用 inject 少用 @Inject 和 Provider.deps。