這些被注入服務(wù)的消費(fèi)者不需要知道如何創(chuàng)建這個(gè)服務(wù)。新建和緩存這個(gè)服務(wù)是依賴(lài)注入器的工作。消費(fèi)者只要讓依賴(lài)注入框架知道它需要哪些依賴(lài)項(xiàng)就可以了。
有時(shí)候一個(gè)服務(wù)依賴(lài)其它服務(wù)...而其它服務(wù)可能依賴(lài)另外的更多服務(wù)。 依賴(lài)注入框架會(huì)負(fù)責(zé)正確的順序解析這些嵌套的依賴(lài)項(xiàng)。 在每一步,依賴(lài)的使用者只要在它的構(gòu)造函數(shù)里簡(jiǎn)單聲明它需要什么,框架就會(huì)完成所有剩下的事情。
下面的例子往 AppComponent
里聲明它依賴(lài) LoggerService
和 UserContext
。
Path:"src/app/app.component.ts" 。
constructor(logger: LoggerService, public userContext: UserContextService) {
userContext.loadUser(this.userId);
logger.logInfo('AppComponent initialized');
}
UserContext
轉(zhuǎn)而依賴(lài) LoggerService
和 UserService
(這個(gè)服務(wù)用來(lái)收集特定用戶(hù)信息)。
Path:"user-context.service.ts (injection)" 。
@Injectable({
providedIn: 'root'
})
export class UserContextService {
constructor(private userService: UserService, private loggerService: LoggerService) {
}
}
當(dāng) Angular 新建 AppComponent
時(shí),依賴(lài)注入框架會(huì)先創(chuàng)建一個(gè) LoggerService
的實(shí)例,然后創(chuàng)建 UserContextService
實(shí)例。 UserContextService
也需要框架剛剛創(chuàng)建的這個(gè) LoggerService
實(shí)例,這樣框架才能為它提供同一個(gè)實(shí)例。UserContextService
還需要框架創(chuàng)建過(guò)的 UserService
。 UserService
沒(méi)有其它依賴(lài),所以依賴(lài)注入框架可以直接 new
出該類(lèi)的一個(gè)實(shí)例,并把它提供給 UserContextService
的構(gòu)造函數(shù)。
父組件 AppComponent
不需要了解這些依賴(lài)的依賴(lài)。 只要在構(gòu)造函數(shù)中聲明自己需要的依賴(lài)即可(這里是 LoggerService
和 UserContextService
),框架會(huì)幫你解析這些嵌套的依賴(lài)。
當(dāng)所有的依賴(lài)都就位之后,AppComponent
就會(huì)顯示該用戶(hù)的信息。
Angular 應(yīng)用程序有多個(gè)依賴(lài)注入器,組織成一個(gè)與組件樹(shù)平行的樹(shù)狀結(jié)構(gòu)。 每個(gè)注入器都會(huì)創(chuàng)建依賴(lài)的一個(gè)單例。在所有該注入器負(fù)責(zé)提供服務(wù)的地方,所提供的都是同一個(gè)實(shí)例。 可以在注入器樹(shù)的任何層級(jí)提供和建立特定的服務(wù)。這意味著,如果在多個(gè)注入器中提供該服務(wù),那么該服務(wù)也就會(huì)有多個(gè)實(shí)例。
由根注入器提供的依賴(lài)可以注入到應(yīng)用中任何地方的任何組件中。 但有時(shí)候你可能希望把服務(wù)的有效性限制到應(yīng)用程序的一個(gè)特定區(qū)域。 比如,你可能希望用戶(hù)明確選擇一個(gè)服務(wù),而不是讓根注入器自動(dòng)提供它。
通過(guò)在組件樹(shù)的子級(jí)根組件中提供服務(wù),可以把一個(gè)被注入服務(wù)的作用域局限在應(yīng)用程序結(jié)構(gòu)中的某個(gè)分支中。 這個(gè)例子中展示了如何通過(guò)把服務(wù)添加到子組件 @Component()
裝飾器的 providers
數(shù)組中,來(lái)為 HeroesBaseComponent
提供另一個(gè) HeroService
實(shí)例:
Path:"src/app/sorted-heroes.component.ts (HeroesBaseComponent excerpt)" 。
@Component({
selector: 'app-unsorted-heroes',
template: `<div *ngFor="let hero of heroes">{{hero.name}}</div>`,
providers: [HeroService]
})
export class HeroesBaseComponent implements OnInit {
constructor(private heroService: HeroService) { }
}
當(dāng) Angular 新建 HeroBaseComponent
的時(shí)候,它會(huì)同時(shí)新建一個(gè) HeroService
實(shí)例,該實(shí)例只在該組件及其子組件(如果有)中可見(jiàn)。
也可以在應(yīng)用程序別處的另一個(gè)組件里提供 HeroService
。這樣就會(huì)導(dǎo)致在另一個(gè)注入器中存在該服務(wù)的另一個(gè)實(shí)例。
這個(gè)例子中,局部化的
HeroService
單例,遍布整份范例代碼,包括HeroBiosComponent
、HeroOfTheMonthComponent
和HeroBaseComponent
。 這些組件每個(gè)都有自己的HeroService
實(shí)例,用來(lái)管理獨(dú)立的英雄庫(kù)。
在組件樹(shù)的同一個(gè)級(jí)別上,有時(shí)需要一個(gè)服務(wù)的多個(gè)實(shí)例。
一個(gè)用來(lái)保存其伴生組件的實(shí)例狀態(tài)的服務(wù)就是個(gè)好例子。 每個(gè)組件都需要該服務(wù)的單獨(dú)實(shí)例。 每個(gè)服務(wù)有自己的工作狀態(tài),與其它組件的服務(wù)和狀態(tài)隔離。這叫做沙箱化,因?yàn)槊總€(gè)服務(wù)和組件實(shí)例都在自己的沙箱里運(yùn)行。
在這個(gè)例子中,HeroBiosComponent
渲染了 HeroBioComponent
的三個(gè)實(shí)例。
Path:"ap/hero-bios.component.ts" 。
@Component({
selector: 'app-hero-bios',
template: `
<app-hero-bio [heroId]="1"></app-hero-bio>
<app-hero-bio [heroId]="2"></app-hero-bio>
<app-hero-bio [heroId]="3"></app-hero-bio>`,
providers: [HeroService]
})
export class HeroBiosComponent {
}
每個(gè) HeroBioComponent
都能編輯一個(gè)英雄的生平。HeroBioComponent
依賴(lài) HeroCacheService
服務(wù)來(lái)對(duì)該英雄進(jìn)行讀取、緩存和執(zhí)行其它持久化操作。
Path:"src/app/hero-cache.service.ts" 。
@Injectable()
export class HeroCacheService {
hero: Hero;
constructor(private heroService: HeroService) {}
fetchCachedHero(id: number) {
if (!this.hero) {
this.hero = this.heroService.getHeroById(id);
}
return this.hero;
}
}
這三個(gè) HeroBioComponent
實(shí)例不能共享同一個(gè) HeroCacheService
實(shí)例。否則它們會(huì)相互沖突,爭(zhēng)相把自己的英雄放在緩存里面。
它們應(yīng)該通過(guò)在自己的元數(shù)據(jù)(metadata)providers
數(shù)組里面列出 HeroCacheService
, 這樣每個(gè) HeroBioComponent
就能擁有自己獨(dú)立的 HeroCacheService
實(shí)例了。
Path:"src/app/hero-bio.component.ts" 。
@Component({
selector: 'app-hero-bio',
template: `
<h4>{{hero.name}}</h4>
<ng-content></ng-content>
<textarea cols="25" [(ngModel)]="hero.description"></textarea>`,
providers: [HeroCacheService]
})
export class HeroBioComponent implements OnInit {
@Input() heroId: number;
constructor(private heroCache: HeroCacheService) { }
ngOnInit() { this.heroCache.fetchCachedHero(this.heroId); }
get hero() { return this.heroCache.hero; }
}
父組件 HeroBiosComponent
把一個(gè)值綁定到 heroId
。ngOnInit
把該 id
傳遞到服務(wù),然后服務(wù)獲取和緩存英雄。hero
屬性的 getter
從服務(wù)里面獲取緩存的英雄,并在模板里顯示它綁定到屬性值。
確認(rèn)三個(gè) HeroBioComponent
實(shí)例擁有自己獨(dú)立的英雄數(shù)據(jù)緩存。
當(dāng)類(lèi)需要某個(gè)依賴(lài)項(xiàng)時(shí),該依賴(lài)項(xiàng)就會(huì)作為參數(shù)添加到類(lèi)的構(gòu)造函數(shù)中。 當(dāng) Angular 需要實(shí)例化該類(lèi)時(shí),就會(huì)調(diào)用 DI
框架來(lái)提供該依賴(lài)。 默認(rèn)情況下,DI
框架會(huì)在注入器樹(shù)中查找一個(gè)提供者,從該組件的局部注入器開(kāi)始,如果需要,則沿著注入器樹(shù)向上冒泡,直到根注入器。
DI
框架將會(huì)拋出一個(gè)錯(cuò)誤。通過(guò)在類(lèi)的構(gòu)造函數(shù)中對(duì)服務(wù)參數(shù)使用參數(shù)裝飾器,可以提供一些選項(xiàng)來(lái)修改默認(rèn)的搜索行為。
依賴(lài)可以注冊(cè)在組件樹(shù)的任何層級(jí)上。 當(dāng)組件請(qǐng)求某個(gè)依賴(lài)時(shí),Angular 會(huì)從該組件的注入器找起,沿著注入器樹(shù)向上,直到找到了第一個(gè)滿(mǎn)足要求的提供者。如果沒(méi)找到依賴(lài),Angular 就會(huì)拋出一個(gè)錯(cuò)誤。
某些情況下,你需要限制搜索,或容忍依賴(lài)項(xiàng)的缺失。 你可以使用組件構(gòu)造函數(shù)參數(shù)上的 @Host
和 @Optional
這兩個(gè)限定裝飾器來(lái)修改 Angular 的搜索行為。
@Optional
屬性裝飾器告訴 Angular 當(dāng)找不到依賴(lài)時(shí)就返回 null
。@Host
屬性裝飾器會(huì)禁止在宿主組件以上的搜索。宿主組件通常就是請(qǐng)求該依賴(lài)的那個(gè)組件。 不過(guò),當(dāng)該組件投影進(jìn)某個(gè)父組件時(shí),那個(gè)父組件就會(huì)變成宿主。下面的例子中介紹了第二種情況。
如下例所示,這些裝飾器可以獨(dú)立使用,也可以同時(shí)使用。這個(gè) HeroBiosAndContactsComponent
是你以前見(jiàn)過(guò)的那個(gè) HeroBiosComponent
的修改版。
Path:"src/app/hero-bios.component.ts (HeroBiosAndContactsComponent)" 。
@Component({
selector: 'app-hero-bios-and-contacts',
template: `
<app-hero-bio [heroId]="1"> <app-hero-contact></app-hero-contact> </app-hero-bio>
<app-hero-bio [heroId]="2"> <app-hero-contact></app-hero-contact> </app-hero-bio>
<app-hero-bio [heroId]="3"> <app-hero-contact></app-hero-contact> </app-hero-bio>`,
providers: [HeroService]
})
export class HeroBiosAndContactsComponent {
constructor(logger: LoggerService) {
logger.logInfo('Creating HeroBiosAndContactsComponent');
}
}
注意看模板:
Path:"dependency-injection-in-action/src/app/hero-bios.component.ts" 。
template: `
<app-hero-bio [heroId]="1"> <app-hero-contact></app-hero-contact> </app-hero-bio>
<app-hero-bio [heroId]="2"> <app-hero-contact></app-hero-contact> </app-hero-bio>
<app-hero-bio [heroId]="3"> <app-hero-contact></app-hero-contact> </app-hero-bio>`,
在 <hero-bio>
標(biāo)簽中是一個(gè)新的 <hero-contact>
元素。Angular 就會(huì)把相應(yīng)的 HeroContactComponent
投影(transclude
)進(jìn) HeroBioComponent
的視圖里, 將它放在 HeroBioComponent
模板的 <ng-content>
標(biāo)簽槽里。
Path:"src/app/hero-bio.component.ts (template)" 。
template: `
<h4>{{hero.name}}</h4>
<ng-content></ng-content>
<textarea cols="25" [(ngModel)]="hero.description"></textarea>`,
從 HeroContactComponent
獲得的英雄電話(huà)號(hào)碼,被投影到上面的英雄描述里,結(jié)果如下:
這里的 HeroContactComponent
演示了限定型裝飾器。
Path:"src/app/hero-contact.component.ts" 。
@Component({
selector: 'app-hero-contact',
template: `
<div>Phone #: {{phoneNumber}}
<span *ngIf="hasLogger">!!!</span></div>`
})
export class HeroContactComponent {
hasLogger = false;
constructor(
@Host() // limit to the host component's instance of the HeroCacheService
private heroCache: HeroCacheService,
@Host() // limit search for logger; hides the application-wide logger
@Optional() // ok if the logger doesn't exist
private loggerService?: LoggerService
) {
if (loggerService) {
this.hasLogger = true;
loggerService.logInfo('HeroContactComponent can log!');
}
}
get phoneNumber() { return this.heroCache.hero.phone; }
}
注意構(gòu)造函數(shù)的參數(shù)。
Path:"src/app/hero-contact.component.ts" 。
@Host() // limit to the host component's instance of the HeroCacheService
private heroCache: HeroCacheService,
@Host() // limit search for logger; hides the application-wide logger
@Optional() // ok if the logger doesn't exist
private loggerService?: LoggerService
@Host()
函數(shù)是構(gòu)造函數(shù)屬性 heroCache
的裝飾器,確保從其父組件 HeroBioComponent
得到一個(gè)緩存服務(wù)。如果該父組件中沒(méi)有該服務(wù),Angular 就會(huì)拋出錯(cuò)誤,即使組件樹(shù)里的再上級(jí)有某個(gè)組件擁有這個(gè)服務(wù),還是會(huì)拋出錯(cuò)誤。
另一個(gè) @Host()
函數(shù)是構(gòu)造函數(shù)屬性 loggerService
的裝飾器。 在本應(yīng)用程序中只有一個(gè)在 AppComponent
級(jí)提供的 LoggerService
實(shí)例。 該宿主 HeroBioComponent
沒(méi)有自己的 LoggerService
提供者。
如果沒(méi)有同時(shí)使用 @Optional()
裝飾器的話(huà),Angular 就會(huì)拋出錯(cuò)誤。當(dāng)該屬性帶有 @Optional()
標(biāo)記時(shí),Angular 就會(huì)把 loggerService
設(shè)置為 null
,并繼續(xù)執(zhí)行組件而不會(huì)拋出錯(cuò)誤。
下面是 HeroBiosAndContactsComponent
的執(zhí)行結(jié)果:
如果注釋掉 @Host()
裝飾器,Angular 就會(huì)沿著注入器樹(shù)往上走,直到在 AppComponent
中找到該日志服務(wù)。日志服務(wù)的邏輯加了進(jìn)來(lái),所顯示的英雄信息增加了 "!!!" 標(biāo)記,這表明確實(shí)找到了日志服務(wù)。
如果你恢復(fù)了 @Host()
裝飾器,并且注釋掉 @Optional 裝飾器,應(yīng)用就會(huì)拋出一個(gè)錯(cuò)誤,因?yàn)樗谒拗鹘M件這一層找不到所需的 Logger
。EXCEPTION: No provider for LoggerService! (HeroContactComponent -> LoggerService)
自定義提供者讓你可以為隱式依賴(lài)提供一個(gè)具體的實(shí)現(xiàn),比如內(nèi)置瀏覽器 API。下面的例子使用 InjectionToken
來(lái)提供 localStorage
,將其作為 BrowserStorageService
的依賴(lài)項(xiàng)。
Path:"src/app/storage.service.ts" 。
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
函數(shù)返回 window
對(duì)象上的 localStorage
屬性。Inject
裝飾器修飾一個(gè)構(gòu)造函數(shù)參數(shù),用于為某個(gè)依賴(lài)提供自定義提供者。現(xiàn)在,就可以在測(cè)試期間使用 localStorage
的 Mock API 來(lái)覆蓋這個(gè)提供者了,而不必與真實(shí)的瀏覽器 API 進(jìn)行交互。
注入器也可以通過(guò)構(gòu)造函數(shù)的參數(shù)裝飾器來(lái)指定范圍。下面的例子就在 Component
類(lèi)的 providers
中使用瀏覽器的 sessionStorage API 覆蓋了 BROWSER_STORAGE
令牌。同一個(gè) BrowserStorageService
在構(gòu)造函數(shù)中使用 @Self
和 @SkipSelf
裝飾器注入了兩次,來(lái)分別指定由哪個(gè)注入器來(lái)提供依賴(lài)。
Path:"src/app/storage.component.ts" 。
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,
@SkipSelf() private localStorageService: BrowserStorageService,
) { }
ngOnInit() {
}
setSession() {
this.sessionStorageService.set('hero', 'Dr Nice - Session');
}
setLocal() {
this.localStorageService.set('hero', 'Dr Nice - Local');
}
}
使用 @Self
裝飾器時(shí),注入器只在該組件的注入器中查找提供者。@SkipSelf
裝飾器可以讓你跳過(guò)局部注入器,并在注入器樹(shù)中向上查找,以發(fā)現(xiàn)哪個(gè)提供者滿(mǎn)足該依賴(lài)。 sessionStorageService
實(shí)例使用瀏覽器的 sessionStorage
來(lái)跟 BrowserStorageService
打交道,而 localStorageService
跳過(guò)了局部注入器,使用根注入器提供的 BrowserStorageService
,它使用瀏覽器的 localStorage API。
即便開(kāi)發(fā)者極力避免,仍然會(huì)有很多視覺(jué)效果和第三方工具 (比如 jQuery) 需要訪(fǎng)問(wèn) DOM。這會(huì)讓你不得不訪(fǎng)問(wèn)組件所在的 DOM 元素。
為了說(shuō)明這一點(diǎn),請(qǐng)看屬性型指令中那個(gè) HighlightDirective
的簡(jiǎn)化版。
Path:"src/app/highlight.directive.ts" 。
import { Directive, ElementRef, HostListener, Input } from '@angular/core';
@Directive({
selector: '[appHighlight]'
})
export class HighlightDirective {
@Input('appHighlight') highlightColor: string;
private el: HTMLElement;
constructor(el: ElementRef) {
this.el = el.nativeElement;
}
@HostListener('mouseenter') onMouseEnter() {
this.highlight(this.highlightColor || 'cyan');
}
@HostListener('mouseleave') onMouseLeave() {
this.highlight(null);
}
private highlight(color: string) {
this.el.style.backgroundColor = color;
}
}
當(dāng)用戶(hù)把鼠標(biāo)移到 DOM 元素上時(shí),指令將指令所在的元素的背景設(shè)置為一個(gè)高亮顏色。
Angular 把構(gòu)造函數(shù)參數(shù) el
設(shè)置為注入的 ElementRef
,該 ElementRef
代表了宿主的 DOM 元素,它的 nativeElement
屬性把該 DOM 元素暴露給了指令。
下面的代碼把指令的 myHighlight
屬性(Attribute)填加到兩個(gè) <div>
標(biāo)簽里,一個(gè)沒(méi)有賦值,一個(gè)賦值了顏色。
Path:"src/app/app.component.html (highlight)" 。
<div id="highlight" class="di-component" appHighlight>
<h3>Hero Bios and Contacts</h3>
<div appHighlight="yellow">
<app-hero-bios-and-contacts></app-hero-bios-and-contacts>
</div>
</div>
下圖顯示了鼠標(biāo)移到 <hero-bios-and-contacts>
標(biāo)簽上的效果:
為了從依賴(lài)注入器中獲取服務(wù),你必須傳給它一個(gè)令牌。 Angular 通常會(huì)通過(guò)指定構(gòu)造函數(shù)參數(shù)以及參數(shù)的類(lèi)型來(lái)處理它。 參數(shù)的類(lèi)型可以用作注入器的查閱令牌。 Angular 會(huì)把該令牌傳給注入器,并把它的結(jié)果賦給相應(yīng)的參數(shù)。
下面是一個(gè)典型的例子。
Path:"src/app/hero-bios.component.ts (component constructor injection)" 。
constructor(logger: LoggerService) {
logger.logInfo('Creating HeroBiosComponent');
}
Angular 會(huì)要求注入器提供與 LoggerService
相關(guān)的服務(wù),并把返回的值賦給 logger
參數(shù)。
如果注入器已經(jīng)緩存了與該令牌相關(guān)的服務(wù)實(shí)例,那么它就會(huì)直接提供此實(shí)例。 如果它沒(méi)有,它就要使用與該令牌相關(guān)的提供者來(lái)創(chuàng)建一個(gè)。
如果注入器無(wú)法根據(jù)令牌在自己內(nèi)部找到對(duì)應(yīng)的提供者,它便將請(qǐng)求移交給它的父級(jí)注入器,這個(gè)過(guò)程不斷重復(fù),直到?jīng)]有更多注入器為止。 如果沒(méi)找到,注入器就拋出一個(gè)錯(cuò)誤...除非這個(gè)請(qǐng)求是可選的。
新的注入器沒(méi)有提供者。 Angular 會(huì)使用一組首選提供者來(lái)初始化它本身的注入器。 你必須為自己應(yīng)用程序特有的依賴(lài)項(xiàng)來(lái)配置提供者。
用于實(shí)例化類(lèi)的默認(rèn)方法不一定總適合用來(lái)創(chuàng)建依賴(lài)。你可以到依賴(lài)提供者部分查看其它方法。 HeroOfTheMonthComponent
例子示范了一些替代方案,展示了為什么需要它們。 它看起來(lái)很簡(jiǎn)單:一些屬性和一些由 logger 生成的日志。
它背后的代碼定制了 DI
框架提供依賴(lài)項(xiàng)的方法和位置。 這個(gè)例子闡明了通過(guò)提供對(duì)象字面量來(lái)把對(duì)象的定義和 DI
令牌關(guān)聯(lián)起來(lái)的另一種方式。
Path:"hero-of-the-month.component.ts" 。
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');
}
}
providers
數(shù)組展示了你可以如何使用其它的鍵來(lái)定義提供者:useValue
、useClass
、useExisting
或 useFactory
。
useValue
鍵讓你可以為 DI
令牌關(guān)聯(lián)一個(gè)固定的值。 使用該技巧來(lái)進(jìn)行運(yùn)行期常量設(shè)置,比如網(wǎng)站的基礎(chǔ)地址和功能標(biāo)志等。 你也可以在單元測(cè)試中使用值提供者,來(lái)用一個(gè) Mock
數(shù)據(jù)來(lái)代替一個(gè)生產(chǎn)環(huán)境下的數(shù)據(jù)服務(wù)。
HeroOfTheMonthComponent
例子中有兩個(gè)值-提供者。
Path:"dependency-injection-in-action/src/app/hero-of-the-month.component.ts" 。
{ provide: Hero, useValue: someHero },
{ provide: TITLE, useValue: 'Hero of the Month' },
Hero
令牌的 Hero
類(lèi)的現(xiàn)有實(shí)例,而不是要求注入器使用 new
來(lái)創(chuàng)建一個(gè)新實(shí)例或使用它自己的緩存實(shí)例。這里令牌就是這個(gè)類(lèi)本身。TITLE
令牌指定了一個(gè)字符串字面量資源。 TITLE
提供者的令牌不是一個(gè)類(lèi),而是一個(gè)特別的提供者查詢(xún)鍵,名叫InjectionToken
,表示一個(gè) InjectionToken
實(shí)例。
你可以把 InjectionToken
用作任何類(lèi)型的提供者的令牌,但是當(dāng)依賴(lài)是簡(jiǎn)單類(lèi)型(比如字符串、數(shù)字、函數(shù))時(shí),它會(huì)特別有用。
一個(gè)值-提供者的值必須在指定之前定義。 比如標(biāo)題字符串就是立即可用的。 該例中的 someHero
變量是以前在如下的文件中定義的。 你不能使用那些要等以后才能定義其值的變量。
Path:"dependency-injection-in-action/src/app/hero-of-the-month.component.ts" 。
const someHero = new Hero(42, 'Magma', 'Had a great month!', '555-555-5555');
其它類(lèi)型的提供者都會(huì)惰性創(chuàng)建它們的值,也就是說(shuō)只在需要注入它們的時(shí)候才創(chuàng)建。
useClass
提供的鍵讓你可以創(chuàng)建并返回指定類(lèi)的新實(shí)例。
你可以使用這類(lèi)提供者來(lái)為公共類(lèi)或默認(rèn)類(lèi)換上一個(gè)替代實(shí)現(xiàn)。比如,這個(gè)替代實(shí)現(xiàn)可以實(shí)現(xiàn)一種不同的策略來(lái)擴(kuò)展默認(rèn)類(lèi),或在測(cè)試環(huán)境中模擬真實(shí)類(lèi)的行為。
請(qǐng)看下面 HeroOfTheMonthComponent
里的兩個(gè)例子:
Path:"dependency-injection-in-action/src/app/hero-of-the-month.component.ts" 。
{ provide: HeroService, useClass: HeroService },
{ provide: LoggerService, useClass: DateLoggerService },
第一個(gè)提供者是展開(kāi)了語(yǔ)法糖的,是一個(gè)典型情況的展開(kāi)。一般來(lái)說(shuō),被新建的類(lèi)(HeroService
)同時(shí)也是該提供者的注入令牌。 通常都選用縮寫(xiě)形式,完整形式可以讓細(xì)節(jié)更明確。
第二個(gè)提供者使用 DateLoggerService
來(lái)滿(mǎn)足 LoggerService
。該 LoggerService
在 AppComponent
級(jí)別已經(jīng)被注冊(cè)。當(dāng)這個(gè)組件要求 LoggerService
的時(shí)候,它得到的卻是 DateLoggerService
服務(wù)的實(shí)例。
這個(gè)組件及其子組件會(huì)得到
DateLoggerService
實(shí)例。這個(gè)組件樹(shù)之外的組件得到的仍是LoggerService
實(shí)例。
DateLoggerService
從 LoggerService
繼承;它把當(dāng)前的日期/時(shí)間附加到每條信息上。
Path:"src/app/date-logger.service.ts" 。
@Injectable({
providedIn: 'root'
})
export class DateLoggerService extends LoggerService
{
logInfo(msg: any) { super.logInfo(stamp(msg)); }
logDebug(msg: any) { super.logInfo(stamp(msg)); }
logError(msg: any) { super.logError(stamp(msg)); }
}
function stamp(msg: any) { return msg + ' at ' + new Date(); }
useExisting
提供了一個(gè)鍵,讓你可以把一個(gè)令牌映射成另一個(gè)令牌。實(shí)際上,第一個(gè)令牌就是第二個(gè)令牌所關(guān)聯(lián)的服務(wù)的別名,這樣就創(chuàng)建了訪(fǎng)問(wèn)同一個(gè)服務(wù)對(duì)象的兩種途徑。
Path:"dependency-injection-in-action/src/app/hero-of-the-month.component.ts" 。
{ provide: MinimalLogger, useExisting: LoggerService },
你可以使用別名接口來(lái)窄化 API。下面的例子中使用別名就是為了這個(gè)目的。
想象 LoggerService
有個(gè)很大的 API 接口,遠(yuǎn)超過(guò)現(xiàn)有的三個(gè)方法和一個(gè)屬性。你可能希望把 API 接口收窄到只有兩個(gè)你確實(shí)需要的成員。在這個(gè)例子中,MinimalLogger
類(lèi)-接口,就這個(gè) API 成功縮小到了只有兩個(gè)成員:
Path:"src/app/minimal-logger.service.ts" 。
// Class used as a "narrowing" interface that exposes a minimal logger
// Other members of the actual implementation are invisible
export abstract class MinimalLogger {
logs: string[];
logInfo: (msg: string) => void;
}
下面的例子在一個(gè)簡(jiǎn)化版的 HeroOfTheMonthComponent
中使用 MinimalLogger
。
Path:"src/app/hero-of-the-month.component.ts (minimal version)" 。
@Component({
selector: 'app-hero-of-the-month',
templateUrl: './hero-of-the-month.component.html',
// TODO: move this aliasing, `useExisting` provider to the AppModule
providers: [{ provide: MinimalLogger, useExisting: LoggerService }]
})
export class HeroOfTheMonthComponent {
logs: string[] = [];
constructor(logger: MinimalLogger) {
logger.logInfo('starting up');
}
}
HeroOfTheMonthComponent
構(gòu)造函數(shù)的 logger
參數(shù)是一個(gè) MinimalLogger
類(lèi)型,在支持 TypeScript 感知的編輯器里,只能看到它的兩個(gè)成員 logs
和 logInfo
:
實(shí)際上,Angular
把 logger
參數(shù)設(shè)置為注入器里 LoggerService
令牌下注冊(cè)的完整服務(wù),該令牌恰好是以前提供的那個(gè) DateLoggerService
實(shí)例。
在下面的圖片中,顯示了日志日期,可以確認(rèn)這一點(diǎn):
Path:"dependency-injection-in-action/src/app/hero-of-the-month.component.ts" 。
{ provide: RUNNERS_UP, useFactory: runnersUpFactory(2), deps: [Hero, HeroService] }
注入器通過(guò)調(diào)用你用 useFactory
鍵指定的工廠函數(shù)來(lái)提供該依賴(lài)的值。 注意,提供者的這種形態(tài)還有第三個(gè)鍵 deps
,它指定了供 useFactory
函數(shù)使用的那些依賴(lài)。
使用這項(xiàng)技術(shù),可以用包含了一些依賴(lài)服務(wù)和本地狀態(tài)輸入的工廠函數(shù)來(lái)建立一個(gè)依賴(lài)對(duì)象。
這個(gè)依賴(lài)對(duì)象(由工廠函數(shù)返回的)通常是一個(gè)類(lèi)實(shí)例,不過(guò)也可以是任何其它東西。 在這個(gè)例子中,依賴(lài)對(duì)象是一個(gè)表示 "月度英雄" 參賽者名稱(chēng)的字符串。
在這個(gè)例子中,局部狀態(tài)是數(shù)字 2,也就是組件應(yīng)該顯示的參賽者數(shù)量。 該狀態(tài)的值傳給了 runnersUpFactory()
作為參數(shù)。 runnersUpFactory()
返回了提供者的工廠函數(shù),它可以使用傳入的狀態(tài)值和注入的服務(wù) Hero
和 HeroService
。
Path:"runners-up.ts (excerpt)" 。
export function runnersUpFactory(take: number) {
return (winner: Hero, heroService: HeroService): string => {
/* ... */
};
};
由 runnersUpFactory()
返回的提供者的工廠函數(shù)返回了實(shí)際的依賴(lài)對(duì)象,也就是表示名字的字符串。
Hero
和一個(gè) HeroService
參數(shù)。
Angular 根據(jù) deps
數(shù)組中指定的兩個(gè)令牌來(lái)提供這些注入?yún)?shù)。
HeroOfTheMonthComponent
的 runnersUp
參數(shù)中。該函數(shù)從
HeroService
中接受候選的英雄,從中取 2 個(gè)參加競(jìng)賽,并把他們的名字串接起來(lái)返回。
當(dāng)使用類(lèi)作為令牌,同時(shí)也把它作為返回依賴(lài)對(duì)象或服務(wù)的類(lèi)型時(shí),Angular 依賴(lài)注入使用起來(lái)最容易。
但令牌不一定都是類(lèi),就算它是一個(gè)類(lèi),它也不一定都返回類(lèi)型相同的對(duì)象。這是下一節(jié)的主題。
前面的月度英雄的例子使用了 MinimalLogger
類(lèi)作為 LoggerService
提供者的令牌。
Path:"dependency-injection-in-action/src/app/hero-of-the-month.component.ts" 。
{ provide: MinimalLogger, useExisting: LoggerService },
該 MinimalLogger
是一個(gè)抽象類(lèi)。
Path:"dependency-injection-in-action/src/app/minimal-logger.service.ts" 。
// Class used as a "narrowing" interface that exposes a minimal logger
// Other members of the actual implementation are invisible
export abstract class MinimalLogger {
logs: string[];
logInfo: (msg: string) => void;
}
你通常從一個(gè)可擴(kuò)展的抽象類(lèi)繼承。但這個(gè)應(yīng)用中并沒(méi)有類(lèi)會(huì)繼承 MinimalLogger
。
LoggerService
和 DateLoggerService
本可以從 MinimalLogger
中繼承。 它們也可以實(shí)現(xiàn) MinimalLogger
,而不用單獨(dú)定義接口。 但它們沒(méi)有。 MinimalLogger
在這里僅僅被用作一個(gè) "依賴(lài)注入令牌"。
當(dāng)你通過(guò)這種方式使用類(lèi)時(shí),它稱(chēng)作類(lèi)接口。
就像 DI 提供者中提到的那樣,接口不是有效的 DI 令牌,因?yàn)樗?TypeScript 自己用的,在運(yùn)行期間不存在。使用這種抽象類(lèi)接口不但可以獲得像接口一樣的強(qiáng)類(lèi)型,而且可以像普通類(lèi)一樣把它用作提供者令牌。
類(lèi)接口應(yīng)該只定義允許它的消費(fèi)者調(diào)用的成員。窄的接口有助于解耦該類(lèi)的具體實(shí)現(xiàn)和它的消費(fèi)者。
用類(lèi)作為接口可以讓你獲得真實(shí) JavaScript 對(duì)象中的接口的特性。 但是,為了最小化內(nèi)存開(kāi)銷(xiāo),該類(lèi)應(yīng)該是沒(méi)有實(shí)現(xiàn)的。 對(duì)于構(gòu)造函數(shù),MinimalLogger
會(huì)轉(zhuǎn)譯成未優(yōu)化過(guò)的、預(yù)先最小化過(guò)的 JavaScript。
Path:"dependency-injection-in-action/src/app/minimal-logger.service.ts" 。
var MinimalLogger = (function () {
function MinimalLogger() {}
return MinimalLogger;
}());
exports("MinimalLogger", MinimalLogger);
注:
只要不實(shí)現(xiàn)它,不管添加多少成員,它都不會(huì)增長(zhǎng)大小,因?yàn)檫@些成員雖然是有類(lèi)型的,但卻沒(méi)有實(shí)現(xiàn)。
你可以再看看 TypeScript 的
MinimalLogger
類(lèi),確定一下它是沒(méi)有實(shí)現(xiàn)的。
依賴(lài)對(duì)象可以是一個(gè)簡(jiǎn)單的值,比如日期,數(shù)字和字符串,或者一個(gè)無(wú)形的對(duì)象,比如數(shù)組和函數(shù)。
這樣的對(duì)象沒(méi)有應(yīng)用程序接口,所以不能用一個(gè)類(lèi)來(lái)表示。更適合表示它們的是:唯一的和符號(hào)性的令牌,一個(gè) JavaScript 對(duì)象,擁有一個(gè)友好的名字,但不會(huì)與其它的同名令牌發(fā)生沖突。
InjectionToken
具有這些特征。在Hero of the Month例子中遇見(jiàn)它們兩次,一個(gè)是 title
的值,一個(gè)是 runnersUp
工廠提供者。
Path:"dependency-injection-in-action/src/app/hero-of-the-month.component.ts" 。
{ provide: TITLE, useValue: 'Hero of the Month' },
{ provide: RUNNERS_UP, useFactory: runnersUpFactory(2), deps: [Hero, HeroService] }
這樣創(chuàng)建 TITLE 令牌:
Path:"dependency-injection-in-action/src/app/hero-of-the-month.component.ts" 。
import { InjectionToken } from '@angular/core';
export const TITLE = new InjectionToken<string>('title');
類(lèi)型參數(shù),雖然是可選的,但可以向開(kāi)發(fā)者和開(kāi)發(fā)工具傳達(dá)類(lèi)型信息。 而且這個(gè)令牌的描述信息也可以為開(kāi)發(fā)者提供幫助。
當(dāng)編寫(xiě)一個(gè)繼承自另一個(gè)組件的組件時(shí),要格外小心。如果基礎(chǔ)組件有依賴(lài)注入,必須要在派生類(lèi)中重新提供和重新注入它們,并將它們通過(guò)構(gòu)造函數(shù)傳給基類(lèi)。
在這個(gè)刻意生成的例子里,SortedHeroesComponent
繼承自 HeroesBaseComponent
,顯示一個(gè)被排序的英雄列表。
HeroesBaseComponent
能自己獨(dú)立運(yùn)行。它在自己的實(shí)例里要求 HeroService
,用來(lái)得到英雄,并將他們按照數(shù)據(jù)庫(kù)返回的順序顯示出來(lái)。
Path:"src/app/sorted-heroes.component.ts (HeroesBaseComponent)" 。
@Component({
selector: 'app-unsorted-heroes',
template: `<div *ngFor="let hero of heroes">{{hero.name}}</div>`,
providers: [HeroService]
})
export class HeroesBaseComponent implements OnInit {
constructor(private heroService: HeroService) { }
heroes: Array<Hero>;
ngOnInit() {
this.heroes = this.heroService.getAllHeroes();
this.afterGetHeroes();
}
// Post-process heroes in derived class override.
protected afterGetHeroes() {}
}
讓構(gòu)造函數(shù)保持簡(jiǎn)單
構(gòu)造函數(shù)應(yīng)該只用來(lái)初始化變量。 這條規(guī)則讓組件在測(cè)試環(huán)境中可以放心地構(gòu)造組件,以免在構(gòu)造它們時(shí),無(wú)意中做出一些非常戲劇化的動(dòng)作(比如與服務(wù)器進(jìn)行會(huì)話(huà))。 這就是為什么你要在 ngOnInit 里面調(diào)用 HeroService,而不是在構(gòu)造函數(shù)中。
用戶(hù)希望看到英雄按字母順序排序。與其修改原始的組件,不如派生它,新建 SortedHeroesComponent
,以便展示英雄之前進(jìn)行排序。 SortedHeroesComponent
讓基類(lèi)來(lái)獲取英雄。
可惜,Angular 不能直接在基類(lèi)里直接注入 HeroService
。必須在這個(gè)組件里再次提供 HeroService
,然后通過(guò)構(gòu)造函數(shù)傳給基類(lèi)。
Path:"src/app/sorted-heroes.component.ts (SortedHeroesComponent)" 。
@Component({
selector: 'app-sorted-heroes',
template: `<div *ngFor="let hero of heroes">{{hero.name}}</div>`,
providers: [HeroService]
})
export class SortedHeroesComponent extends HeroesBaseComponent {
constructor(heroService: HeroService) {
super(heroService);
}
protected afterGetHeroes() {
this.heroes = this.heroes.sort((h1, h2) => {
return h1.name < h2.name ? -1 :
(h1.name > h2.name ? 1 : 0);
});
}
}
現(xiàn)在,請(qǐng)注意 afterGetHeroes()
方法。 你的第一反應(yīng)是在 SortedHeroesComponent
組件里面建一個(gè) ngOnInit
方法來(lái)做排序。但是 Angular 會(huì)先調(diào)用派生類(lèi)的 ngOnInit
,后調(diào)用基類(lèi)的 ngOnInit
, 所以可能在英雄到達(dá)之前就開(kāi)始排序。這就產(chǎn)生了一個(gè)討厭的錯(cuò)誤。
覆蓋基類(lèi)的 afterGetHeroes()
方法可以解決這個(gè)問(wèn)題。
分析上面的這些復(fù)雜性是為了強(qiáng)調(diào)避免使用組件繼承這一點(diǎn)。
在 TypeScript 里面,類(lèi)聲明的順序是很重要的。如果一個(gè)類(lèi)尚未定義,就不能引用它。
這通常不是一個(gè)問(wèn)題,特別是當(dāng)你遵循一個(gè)文件一個(gè)類(lèi)規(guī)則的時(shí)候。 但是有時(shí)候循環(huán)引用可能不能避免。當(dāng)一個(gè)類(lèi)A 引用類(lèi) B,同時(shí)'B'引用'A'的時(shí)候,你就陷入困境了:它們中間的某一個(gè)必須要先定義。
Angular 的 forwardRef()
函數(shù)建立一個(gè)間接地引用,Angular 可以隨后解析。
這個(gè)關(guān)于父查找器的例子中全都是沒(méi)辦法打破的循環(huán)類(lèi)引用。
當(dāng)一個(gè)類(lèi)需要引用自身的時(shí)候,你面臨同樣的困境,就像在 AlexComponent
的 provdiers
數(shù)組中遇到的困境一樣。 該 providers
數(shù)組是一個(gè) @Component()
裝飾器函數(shù)的一個(gè)屬性,它必須在類(lèi)定義之前出現(xiàn)。
使用 forwardRef
來(lái)打破這種循環(huán):
Path:"parent-finder.component.ts (AlexComponent providers)" 。
providers: [{ provide: Parent, useExisting: forwardRef(() => AlexComponent) }],
更多建議: