前言
之前我们也学习过了 angular 框架的依赖注入的一些基本使用方式,今天将更加深入的学习下依赖注入
用 @Optional 来让依赖是可选的,以及使用 @Host 来限定搜索方式
依赖可以注册在组件树的任何层级上。 当组件请求某个依赖时,Angular 会从该组件的注入器找起,沿着注入器树向上,直到找到了第一个满足要求的提供商。如果没找到依赖,Angular 就会抛出一个错误。
某些情况下,你需要限制搜索,或容忍依赖项的缺失。 你可以使用组件构造函数参数上的 @Host 和 @Optional 这两个限定装饰器来修改 Angular 的搜索行为。
- @Optional 属性装饰器告诉 Angular 当找不到依赖时就返回 null。
- @Host 属性装饰器会禁止在宿主组件以上的搜索。宿主组件通常就是请求该依赖的那个组件。 不过,当该组件投影进某个父组件时,那个父组件就会变成宿主。
constructor(
@Host() // 查找HeroCacheService实例限制在该组件或者它的父组件以下
private heroCache: HeroCacheService,
@Host()
@Optional() // 限制在该组件的父组件内查找,如果没有找到则返回null,也不会报错
private loggerService: LoggerService
)
使用 @Inject 指定自定义提供商
自定义提供商让你可以为隐式依赖提供一个具体的实现,比如内置浏览器 API。下面的例子使用 InjectionToken 来提供 localStorage,将其作为 BrowserStorageService 的依赖项。
import { Inject, Injectable, InjectionToken } from '@angular/core';
export const BROWSER_STORAGE = new InjectionToken<Storage>('Browser Storage', {
providedIn: 'root',
factory: () => localStorage
});
@Injectable({
providedIn: 'root'
})
export class BrowserStorageService {
constructor(@Inject(BROWSER_STORAGE) public storage: Storage) {}
get(key: string) {
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 装饰器注入了两次,来分别指定由哪个注入器来提供依赖。
import { Component, OnInit, Self, SkipSelf } from '@angular/core';
import { BROWSER_STORAGE, BrowserStorageService } from './storage.service';
@Component({
selector: 'app-storage',
template: `
Open the inspector to see the local/session storage keys:
<h3>Session Storage</h3>
<button (click)="setSession()">Set Session Storage</button>
<h3>Local Storage</h3>
<button (click)="setLocal()">Set Local Storage</button>
`,
providers: [
BrowserStorageService,
{ provide: BROWSER_STORAGE, useFactory: () => sessionStorage }
]
})
export class StorageComponent implements OnInit {
constructor(
@Self() private sessionStorageService: BrowserStorageService, // BrowserStorageService会读取该组件提供的 BROWSER_STORAGE
@SkipSelf() private localStorageService: BrowserStorageService, // BrowserStorageService跳过该组件提供的 BROWSER_STORAGE 则默认读取自己组件的依赖
) { }
ngOnInit() {
}
setSession() {
this.sessionStorageService.set('hero', 'Dr Nice - Session');
}
setLocal() {
this.localStorageService.set('hero', 'Dr Nice - Local');
}
}
使用 @Self 装饰器时,注入器只在该组件的注入器中查找提供商。@SkipSelf 装饰器可以让你跳过局部注入器,并在注入器树中向上查找,以发现哪个提供商满足该依赖。 sessionStorageService 实例使用浏览器的 sessionStorage 来跟 BrowserStorageService 打交道,而 localStorageService 跳过了局部注入器,使用根注入器提供的 BrowserStorageService,它使用浏览器的 localStorage API。
使用提供商来定义依赖
为了从依赖注入器中获取服务,你必须传给它一个令牌。 Angular 通常会通过指定构造函数参数以及参数的类型来处理它。 参数的类型可以用作注入器的查阅令牌。 Angular 会把该令牌传给注入器,并把它的结果赋给相应的参数。
constructor(logger: LoggerService) {
logger.logInfo('Creating HeroBiosComponent');
}
Angular 会要求注入器提供与 LoggerService 相关的服务,并把返回的值赋给 logger 参数。
如果注入器已经缓存了与该令牌相关的服务实例,那么它就会直接提供此实例。 如果它没有,它就要使用与该令牌相关的提供商来创建一个。
定义提供商的几种方式
import { Component, Inject } from '@angular/core';
import { DateLoggerService } from './date-logger.service';
import { Hero } from './hero';
import { HeroService } from './hero.service';
import { LoggerService } from './logger.service';
import { MinimalLogger } from './minimal-logger.service';
import { RUNNERS_UP,
runnersUpFactory } from './runners-up';
@Component({
selector: 'app-hero-of-the-month',
templateUrl: './hero-of-the-month.component.html',
providers: [
{ provide: Hero, useValue: someHero },
{ provide: TITLE, useValue: 'Hero of the Month' },
{ provide: HeroService, useClass: HeroService },
{ provide: LoggerService, useClass: DateLoggerService },
{ provide: MinimalLogger, useExisting: LoggerService },
{ provide: RUNNERS_UP, useFactory: runnersUpFactory(2), deps: [Hero, HeroService] }
]
})
export class HeroOfTheMonthComponent {
logs: string[] = [];
constructor(
logger: MinimalLogger,
public heroOfTheMonth: Hero,
@Inject(RUNNERS_UP) public runnersUp: string,
@Inject(TITLE) public title: string)
{
this.logs = logger.logs;
logger.logInfo('starting up');
}
}
【值提供商:useValue】
useValue 键让你可以为 DI 令牌关联一个固定的值。 使用该技巧来进行运行期常量设置,比如网站的基础地址和功能标志等。 你也可以在单元测试中使用值提供商,来用一个 Mock 数据来代替一个生产环境下的数据服务。
【类提供商:useClass】
useClass 提供的键让你可以创建并返回指定类的新实例。
你可以使用这类提供商来为公共类或默认类换上一个替代实现。比如,这个替代实现可以实现一种不同的策略来扩展默认类,或在测试环境中模拟真实类的行为
【别名提供商:useExisting】
useExisting 提供了一个键,让你可以把一个令牌映射成另一个令牌。实际上,第一个令牌就是第二个令牌所关联的服务的别名,这样就创建了访问同一个服务对象的两种途径。
{ provide: MinimalLogger, useExisting: LoggerService }
MinimalLogger 可以访问 LoggerService 服务
【工厂提供商:useFactory】
useFactory 提供了一个键,让你可以通过调用一个工厂函数来创建依赖实例
{ provide: RUNNERS_UP, useFactory: runnersUpFactory(2), deps: [Hero, HeroService] }
注入器通过调用你用 useFactory 键指定的工厂函数来提供该依赖的值。 注意,提供商的这种形态还有第三个键 deps,它指定了供 useFactory 函数使用的那些依赖。
runners-up.ts
export function runnersUpFactory(take: number) {
return (winner: Hero, heroService: HeroService): string => {
/* ... */
};
};
提供替代令牌:'InjectionToken'
依赖对象可以是一个简单的值,比如日期,数字和字符串,或者一个无形的对象,比如数组和函数。
这样的对象没有应用程序接口,所以不能用一个类来表示。更适合表示它们的是:唯一的和符号性的令牌,一个 JavaScript 对象,拥有一个友好的名字,但不会与其它的同名令牌发生冲突。
InjectionToken 具有这些特征。在Hero of the Month例子中遇见它们两次,一个是 title 的值,一个是 runnersUp 工厂提供商。
{ provide: TITLE, useValue: 'Hero of the Month' },
{ provide: RUNNERS_UP, useFactory: runnersUpFactory(2), deps: [Hero, HeroService] }
这样创建 TITLE 令牌:
import { InjectionToken } from '@angular/core';
export const TITLE = new InjectionToken<string>('title');
使用一个前向引用(forwardRef)来打破循环
在 TypeScript 里面,类声明的顺序是很重要的。如果一个类尚未定义,就不能引用它。
这通常不是一个问题,特别是当你遵循一个文件一个类规则的时候。 但是有时候循环引用可能不能避免。当一个类A 引用类 B,同时'B'引用'A'的时候,你就陷入困境了:它们中间的某一个必须要先定义。
Angular 的 forwardRef() 函数建立一个间接地引用,Angular 可以随后解析。
providers: [{ provide: Parent, useExisting: forwardRef(() => AlexComponent) }],
forwardRef
允许引用尚未定义的引用
class Door {
lock: Lock;
// Door attempts to inject Lock, despite it not being defined yet.
// forwardRef makes this possible.
constructor(@Inject(forwardRef(() => Lock)) lock: Lock) { this.lock = lock; }
}
// Only at this point Lock is defined.
class Lock {}