這些被注入服務(wù)的消費(fèi)者不需要知道如何創(chuàng)建這個服務(wù)。新建和緩存這個服務(wù)是依賴注入器的工作。消費(fèi)者只要讓依賴注入框架知道它需要哪些依賴項就可以了。
有時候一個服務(wù)依賴其它服務(wù)...而其它服務(wù)可能依賴另外的更多服務(wù)。 依賴注入框架會負(fù)責(zé)正確的順序解析這些嵌套的依賴項。 在每一步,依賴的使用者只要在它的構(gòu)造函數(shù)里簡單聲明它需要什么,框架就會完成所有剩下的事情。
下面的例子往 AppComponent
里聲明它依賴 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)而依賴 LoggerService
和 UserService
(這個服務(wù)用來收集特定用戶信息)。
Path:"user-context.service.ts (injection)" 。
@Injectable({
providedIn: 'root'
})
export class UserContextService {
constructor(private userService: UserService, private loggerService: LoggerService) {
}
}
當(dāng) Angular 新建 AppComponent
時,依賴注入框架會先創(chuàng)建一個 LoggerService
的實例,然后創(chuàng)建 UserContextService
實例。 UserContextService
也需要框架剛剛創(chuàng)建的這個 LoggerService
實例,這樣框架才能為它提供同一個實例。UserContextService
還需要框架創(chuàng)建過的 UserService
。 UserService
沒有其它依賴,所以依賴注入框架可以直接 new
出該類的一個實例,并把它提供給 UserContextService
的構(gòu)造函數(shù)。
父組件 AppComponent
不需要了解這些依賴的依賴。 只要在構(gòu)造函數(shù)中聲明自己需要的依賴即可(這里是 LoggerService
和 UserContextService
),框架會幫你解析這些嵌套的依賴。
當(dāng)所有的依賴都就位之后,AppComponent
就會顯示該用戶的信息。
Angular 應(yīng)用程序有多個依賴注入器,組織成一個與組件樹平行的樹狀結(jié)構(gòu)。 每個注入器都會創(chuàng)建依賴的一個單例。在所有該注入器負(fù)責(zé)提供服務(wù)的地方,所提供的都是同一個實例。 可以在注入器樹的任何層級提供和建立特定的服務(wù)。這意味著,如果在多個注入器中提供該服務(wù),那么該服務(wù)也就會有多個實例。
由根注入器提供的依賴可以注入到應(yīng)用中任何地方的任何組件中。 但有時候你可能希望把服務(wù)的有效性限制到應(yīng)用程序的一個特定區(qū)域。 比如,你可能希望用戶明確選擇一個服務(wù),而不是讓根注入器自動提供它。
通過在組件樹的子級根組件中提供服務(wù),可以把一個被注入服務(wù)的作用域局限在應(yīng)用程序結(jié)構(gòu)中的某個分支中。 這個例子中展示了如何通過把服務(wù)添加到子組件 @Component()
裝飾器的 providers
數(shù)組中,來為 HeroesBaseComponent
提供另一個 HeroService
實例:
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
的時候,它會同時新建一個 HeroService
實例,該實例只在該組件及其子組件(如果有)中可見。
也可以在應(yīng)用程序別處的另一個組件里提供 HeroService
。這樣就會導(dǎo)致在另一個注入器中存在該服務(wù)的另一個實例。
這個例子中,局部化的
HeroService
單例,遍布整份范例代碼,包括HeroBiosComponent
、HeroOfTheMonthComponent
和HeroBaseComponent
。 這些組件每個都有自己的HeroService
實例,用來管理獨(dú)立的英雄庫。
在組件樹的同一個級別上,有時需要一個服務(wù)的多個實例。
一個用來保存其伴生組件的實例狀態(tài)的服務(wù)就是個好例子。 每個組件都需要該服務(wù)的單獨(dú)實例。 每個服務(wù)有自己的工作狀態(tài),與其它組件的服務(wù)和狀態(tài)隔離。這叫做沙箱化,因為每個服務(wù)和組件實例都在自己的沙箱里運(yùn)行。
在這個例子中,HeroBiosComponent
渲染了 HeroBioComponent
的三個實例。
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 {
}
每個 HeroBioComponent
都能編輯一個英雄的生平。HeroBioComponent
依賴 HeroCacheService
服務(wù)來對該英雄進(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;
}
}
這三個 HeroBioComponent
實例不能共享同一個 HeroCacheService
實例。否則它們會相互沖突,爭相把自己的英雄放在緩存里面。
它們應(yīng)該通過在自己的元數(shù)據(jù)(metadata)providers
數(shù)組里面列出 HeroCacheService
, 這樣每個 HeroBioComponent
就能擁有自己獨(dú)立的 HeroCacheService
實例了。
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
把一個值綁定到 heroId
。ngOnInit
把該 id
傳遞到服務(wù),然后服務(wù)獲取和緩存英雄。hero
屬性的 getter
從服務(wù)里面獲取緩存的英雄,并在模板里顯示它綁定到屬性值。
確認(rèn)三個 HeroBioComponent
實例擁有自己獨(dú)立的英雄數(shù)據(jù)緩存。
當(dāng)類需要某個依賴項時,該依賴項就會作為參數(shù)添加到類的構(gòu)造函數(shù)中。 當(dāng) Angular 需要實例化該類時,就會調(diào)用 DI
框架來提供該依賴。 默認(rèn)情況下,DI
框架會在注入器樹中查找一個提供者,從該組件的局部注入器開始,如果需要,則沿著注入器樹向上冒泡,直到根注入器。
DI
框架將會拋出一個錯誤。通過在類的構(gòu)造函數(shù)中對服務(wù)參數(shù)使用參數(shù)裝飾器,可以提供一些選項來修改默認(rèn)的搜索行為。
依賴可以注冊在組件樹的任何層級上。 當(dāng)組件請求某個依賴時,Angular 會從該組件的注入器找起,沿著注入器樹向上,直到找到了第一個滿足要求的提供者。如果沒找到依賴,Angular 就會拋出一個錯誤。
某些情況下,你需要限制搜索,或容忍依賴項的缺失。 你可以使用組件構(gòu)造函數(shù)參數(shù)上的 @Host
和 @Optional
這兩個限定裝飾器來修改 Angular 的搜索行為。
@Optional
屬性裝飾器告訴 Angular 當(dāng)找不到依賴時就返回 null
。@Host
屬性裝飾器會禁止在宿主組件以上的搜索。宿主組件通常就是請求該依賴的那個組件。 不過,當(dāng)該組件投影進(jìn)某個父組件時,那個父組件就會變成宿主。下面的例子中介紹了第二種情況。
如下例所示,這些裝飾器可以獨(dú)立使用,也可以同時使用。這個 HeroBiosAndContactsComponent
是你以前見過的那個 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)簽中是一個新的 <hero-contact>
元素。Angular 就會把相應(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
獲得的英雄電話號碼,被投影到上面的英雄描述里,結(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
得到一個緩存服務(wù)。如果該父組件中沒有該服務(wù),Angular 就會拋出錯誤,即使組件樹里的再上級有某個組件擁有這個服務(wù),還是會拋出錯誤。
另一個 @Host()
函數(shù)是構(gòu)造函數(shù)屬性 loggerService
的裝飾器。 在本應(yīng)用程序中只有一個在 AppComponent
級提供的 LoggerService
實例。 該宿主 HeroBioComponent
沒有自己的 LoggerService
提供者。
如果沒有同時使用 @Optional()
裝飾器的話,Angular 就會拋出錯誤。當(dāng)該屬性帶有 @Optional()
標(biāo)記時,Angular 就會把 loggerService
設(shè)置為 null
,并繼續(xù)執(zhí)行組件而不會拋出錯誤。
下面是 HeroBiosAndContactsComponent
的執(zhí)行結(jié)果:
如果注釋掉 @Host()
裝飾器,Angular 就會沿著注入器樹往上走,直到在 AppComponent
中找到該日志服務(wù)。日志服務(wù)的邏輯加了進(jìn)來,所顯示的英雄信息增加了 "!!!" 標(biāo)記,這表明確實找到了日志服務(wù)。
如果你恢復(fù)了 @Host()
裝飾器,并且注釋掉 @Optional 裝飾器,應(yīng)用就會拋出一個錯誤,因為它在宿主組件這一層找不到所需的 Logger
。EXCEPTION: No provider for LoggerService! (HeroContactComponent -> LoggerService)
自定義提供者讓你可以為隱式依賴提供一個具體的實現(xiàn),比如內(nèi)置瀏覽器 API。下面的例子使用 InjectionToken
來提供 localStorage
,將其作為 BrowserStorageService
的依賴項。
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
對象上的 localStorage
屬性。Inject
裝飾器修飾一個構(gòu)造函數(shù)參數(shù),用于為某個依賴提供自定義提供者?,F(xiàn)在,就可以在測試期間使用 localStorage
的 Mock API 來覆蓋這個提供者了,而不必與真實的瀏覽器 API 進(jìn)行交互。
注入器也可以通過構(gòu)造函數(shù)的參數(shù)裝飾器來指定范圍。下面的例子就在 Component
類的 providers
中使用瀏覽器的 sessionStorage API 覆蓋了 BROWSER_STORAGE
令牌。同一個 BrowserStorageService
在構(gòu)造函數(shù)中使用 @Self
和 @SkipSelf
裝飾器注入了兩次,來分別指定由哪個注入器來提供依賴。
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
裝飾器時,注入器只在該組件的注入器中查找提供者。@SkipSelf
裝飾器可以讓你跳過局部注入器,并在注入器樹中向上查找,以發(fā)現(xiàn)哪個提供者滿足該依賴。 sessionStorageService
實例使用瀏覽器的 sessionStorage
來跟 BrowserStorageService
打交道,而 localStorageService
跳過了局部注入器,使用根注入器提供的 BrowserStorageService
,它使用瀏覽器的 localStorage API。
即便開發(fā)者極力避免,仍然會有很多視覺效果和第三方工具 (比如 jQuery) 需要訪問 DOM。這會讓你不得不訪問組件所在的 DOM 元素。
為了說明這一點(diǎn),請看屬性型指令中那個 HighlightDirective
的簡化版。
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)用戶把鼠標(biāo)移到 DOM 元素上時,指令將指令所在的元素的背景設(shè)置為一個高亮顏色。
Angular 把構(gòu)造函數(shù)參數(shù) el
設(shè)置為注入的 ElementRef
,該 ElementRef
代表了宿主的 DOM 元素,它的 nativeElement
屬性把該 DOM 元素暴露給了指令。
下面的代碼把指令的 myHighlight
屬性(Attribute)填加到兩個 <div>
標(biāo)簽里,一個沒有賦值,一個賦值了顏色。
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)簽上的效果:
為了從依賴注入器中獲取服務(wù),你必須傳給它一個令牌。 Angular 通常會通過指定構(gòu)造函數(shù)參數(shù)以及參數(shù)的類型來處理它。 參數(shù)的類型可以用作注入器的查閱令牌。 Angular 會把該令牌傳給注入器,并把它的結(jié)果賦給相應(yīng)的參數(shù)。
下面是一個典型的例子。
Path:"src/app/hero-bios.component.ts (component constructor injection)" 。
constructor(logger: LoggerService) {
logger.logInfo('Creating HeroBiosComponent');
}
Angular 會要求注入器提供與 LoggerService
相關(guān)的服務(wù),并把返回的值賦給 logger
參數(shù)。
如果注入器已經(jīng)緩存了與該令牌相關(guān)的服務(wù)實例,那么它就會直接提供此實例。 如果它沒有,它就要使用與該令牌相關(guān)的提供者來創(chuàng)建一個。
如果注入器無法根據(jù)令牌在自己內(nèi)部找到對應(yīng)的提供者,它便將請求移交給它的父級注入器,這個過程不斷重復(fù),直到?jīng)]有更多注入器為止。 如果沒找到,注入器就拋出一個錯誤...除非這個請求是可選的。
新的注入器沒有提供者。 Angular 會使用一組首選提供者來初始化它本身的注入器。 你必須為自己應(yīng)用程序特有的依賴項來配置提供者。
用于實例化類的默認(rèn)方法不一定總適合用來創(chuàng)建依賴。你可以到依賴提供者部分查看其它方法。 HeroOfTheMonthComponent
例子示范了一些替代方案,展示了為什么需要它們。 它看起來很簡單:一些屬性和一些由 logger 生成的日志。
它背后的代碼定制了 DI
框架提供依賴項的方法和位置。 這個例子闡明了通過提供對象字面量來把對象的定義和 DI
令牌關(guān)聯(lián)起來的另一種方式。
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ù)組展示了你可以如何使用其它的鍵來定義提供者:useValue
、useClass
、useExisting
或 useFactory
。
useValue
鍵讓你可以為 DI
令牌關(guān)聯(lián)一個固定的值。 使用該技巧來進(jìn)行運(yùn)行期常量設(shè)置,比如網(wǎng)站的基礎(chǔ)地址和功能標(biāo)志等。 你也可以在單元測試中使用值提供者,來用一個 Mock
數(shù)據(jù)來代替一個生產(chǎn)環(huán)境下的數(shù)據(jù)服務(wù)。
HeroOfTheMonthComponent
例子中有兩個值-提供者。
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
類的現(xiàn)有實例,而不是要求注入器使用 new
來創(chuàng)建一個新實例或使用它自己的緩存實例。這里令牌就是這個類本身。TITLE
令牌指定了一個字符串字面量資源。 TITLE
提供者的令牌不是一個類,而是一個特別的提供者查詢鍵,名叫InjectionToken
,表示一個 InjectionToken
實例。
你可以把 InjectionToken
用作任何類型的提供者的令牌,但是當(dāng)依賴是簡單類型(比如字符串、數(shù)字、函數(shù))時,它會特別有用。
一個值-提供者的值必須在指定之前定義。 比如標(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');
其它類型的提供者都會惰性創(chuàng)建它們的值,也就是說只在需要注入它們的時候才創(chuàng)建。
useClass
提供的鍵讓你可以創(chuàng)建并返回指定類的新實例。
你可以使用這類提供者來為公共類或默認(rèn)類換上一個替代實現(xiàn)。比如,這個替代實現(xiàn)可以實現(xiàn)一種不同的策略來擴(kuò)展默認(rèn)類,或在測試環(huán)境中模擬真實類的行為。
請看下面 HeroOfTheMonthComponent
里的兩個例子:
Path:"dependency-injection-in-action/src/app/hero-of-the-month.component.ts" 。
{ provide: HeroService, useClass: HeroService },
{ provide: LoggerService, useClass: DateLoggerService },
第一個提供者是展開了語法糖的,是一個典型情況的展開。一般來說,被新建的類(HeroService
)同時也是該提供者的注入令牌。 通常都選用縮寫形式,完整形式可以讓細(xì)節(jié)更明確。
第二個提供者使用 DateLoggerService
來滿足 LoggerService
。該 LoggerService
在 AppComponent
級別已經(jīng)被注冊。當(dāng)這個組件要求 LoggerService
的時候,它得到的卻是 DateLoggerService
服務(wù)的實例。
這個組件及其子組件會得到
DateLoggerService
實例。這個組件樹之外的組件得到的仍是LoggerService
實例。
DateLoggerService
從 LoggerService
繼承;它把當(dāng)前的日期/時間附加到每條信息上。
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
提供了一個鍵,讓你可以把一個令牌映射成另一個令牌。實際上,第一個令牌就是第二個令牌所關(guān)聯(lián)的服務(wù)的別名,這樣就創(chuàng)建了訪問同一個服務(wù)對象的兩種途徑。
Path:"dependency-injection-in-action/src/app/hero-of-the-month.component.ts" 。
{ provide: MinimalLogger, useExisting: LoggerService },
你可以使用別名接口來窄化 API。下面的例子中使用別名就是為了這個目的。
想象 LoggerService
有個很大的 API 接口,遠(yuǎn)超過現(xiàn)有的三個方法和一個屬性。你可能希望把 API 接口收窄到只有兩個你確實需要的成員。在這個例子中,MinimalLogger
類-接口,就這個 API 成功縮小到了只有兩個成員:
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;
}
下面的例子在一個簡化版的 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ù)是一個 MinimalLogger
類型,在支持 TypeScript 感知的編輯器里,只能看到它的兩個成員 logs
和 logInfo
:
實際上,Angular
把 logger
參數(shù)設(shè)置為注入器里 LoggerService
令牌下注冊的完整服務(wù),該令牌恰好是以前提供的那個 DateLoggerService
實例。
在下面的圖片中,顯示了日志日期,可以確認(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] }
注入器通過調(diào)用你用 useFactory
鍵指定的工廠函數(shù)來提供該依賴的值。 注意,提供者的這種形態(tài)還有第三個鍵 deps
,它指定了供 useFactory
函數(shù)使用的那些依賴。
使用這項技術(shù),可以用包含了一些依賴服務(wù)和本地狀態(tài)輸入的工廠函數(shù)來建立一個依賴對象。
這個依賴對象(由工廠函數(shù)返回的)通常是一個類實例,不過也可以是任何其它東西。 在這個例子中,依賴對象是一個表示 "月度英雄" 參賽者名稱的字符串。
在這個例子中,局部狀態(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ù)返回了實際的依賴對象,也就是表示名字的字符串。
Hero
和一個 HeroService
參數(shù)。
Angular 根據(jù) deps
數(shù)組中指定的兩個令牌來提供這些注入?yún)?shù)。
HeroOfTheMonthComponent
的 runnersUp
參數(shù)中。該函數(shù)從
HeroService
中接受候選的英雄,從中取 2 個參加競賽,并把他們的名字串接起來返回。
當(dāng)使用類作為令牌,同時也把它作為返回依賴對象或服務(wù)的類型時,Angular 依賴注入使用起來最容易。
但令牌不一定都是類,就算它是一個類,它也不一定都返回類型相同的對象。這是下一節(jié)的主題。
前面的月度英雄的例子使用了 MinimalLogger
類作為 LoggerService
提供者的令牌。
Path:"dependency-injection-in-action/src/app/hero-of-the-month.component.ts" 。
{ provide: MinimalLogger, useExisting: LoggerService },
該 MinimalLogger
是一個抽象類。
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;
}
你通常從一個可擴(kuò)展的抽象類繼承。但這個應(yīng)用中并沒有類會繼承 MinimalLogger
。
LoggerService
和 DateLoggerService
本可以從 MinimalLogger
中繼承。 它們也可以實現(xiàn) MinimalLogger
,而不用單獨(dú)定義接口。 但它們沒有。 MinimalLogger
在這里僅僅被用作一個 "依賴注入令牌"。
當(dāng)你通過這種方式使用類時,它稱作類接口。
就像 DI 提供者中提到的那樣,接口不是有效的 DI 令牌,因為它是 TypeScript 自己用的,在運(yùn)行期間不存在。使用這種抽象類接口不但可以獲得像接口一樣的強(qiáng)類型,而且可以像普通類一樣把它用作提供者令牌。
類接口應(yīng)該只定義允許它的消費(fèi)者調(diào)用的成員。窄的接口有助于解耦該類的具體實現(xiàn)和它的消費(fèi)者。
用類作為接口可以讓你獲得真實 JavaScript 對象中的接口的特性。 但是,為了最小化內(nèi)存開銷,該類應(yīng)該是沒有實現(xiàn)的。 對于構(gòu)造函數(shù),MinimalLogger
會轉(zhuǎn)譯成未優(yōu)化過的、預(yù)先最小化過的 JavaScript。
Path:"dependency-injection-in-action/src/app/minimal-logger.service.ts" 。
var MinimalLogger = (function () {
function MinimalLogger() {}
return MinimalLogger;
}());
exports("MinimalLogger", MinimalLogger);
注:
只要不實現(xiàn)它,不管添加多少成員,它都不會增長大小,因為這些成員雖然是有類型的,但卻沒有實現(xiàn)。
你可以再看看 TypeScript 的
MinimalLogger
類,確定一下它是沒有實現(xiàn)的。
依賴對象可以是一個簡單的值,比如日期,數(shù)字和字符串,或者一個無形的對象,比如數(shù)組和函數(shù)。
這樣的對象沒有應(yīng)用程序接口,所以不能用一個類來表示。更適合表示它們的是:唯一的和符號性的令牌,一個 JavaScript 對象,擁有一個友好的名字,但不會與其它的同名令牌發(fā)生沖突。
InjectionToken
具有這些特征。在Hero of the Month例子中遇見它們兩次,一個是 title
的值,一個是 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');
類型參數(shù),雖然是可選的,但可以向開發(fā)者和開發(fā)工具傳達(dá)類型信息。 而且這個令牌的描述信息也可以為開發(fā)者提供幫助。
當(dāng)編寫一個繼承自另一個組件的組件時,要格外小心。如果基礎(chǔ)組件有依賴注入,必須要在派生類中重新提供和重新注入它們,并將它們通過構(gòu)造函數(shù)傳給基類。
在這個刻意生成的例子里,SortedHeroesComponent
繼承自 HeroesBaseComponent
,顯示一個被排序的英雄列表。
HeroesBaseComponent
能自己獨(dú)立運(yùn)行。它在自己的實例里要求 HeroService
,用來得到英雄,并將他們按照數(shù)據(jù)庫返回的順序顯示出來。
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ù)保持簡單
構(gòu)造函數(shù)應(yīng)該只用來初始化變量。 這條規(guī)則讓組件在測試環(huán)境中可以放心地構(gòu)造組件,以免在構(gòu)造它們時,無意中做出一些非常戲劇化的動作(比如與服務(wù)器進(jìn)行會話)。 這就是為什么你要在 ngOnInit 里面調(diào)用 HeroService,而不是在構(gòu)造函數(shù)中。
用戶希望看到英雄按字母順序排序。與其修改原始的組件,不如派生它,新建 SortedHeroesComponent
,以便展示英雄之前進(jìn)行排序。 SortedHeroesComponent
讓基類來獲取英雄。
可惜,Angular 不能直接在基類里直接注入 HeroService
。必須在這個組件里再次提供 HeroService
,然后通過構(gòu)造函數(shù)傳給基類。
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)在,請注意 afterGetHeroes()
方法。 你的第一反應(yīng)是在 SortedHeroesComponent
組件里面建一個 ngOnInit
方法來做排序。但是 Angular 會先調(diào)用派生類的 ngOnInit
,后調(diào)用基類的 ngOnInit
, 所以可能在英雄到達(dá)之前就開始排序。這就產(chǎn)生了一個討厭的錯誤。
覆蓋基類的 afterGetHeroes()
方法可以解決這個問題。
分析上面的這些復(fù)雜性是為了強(qiáng)調(diào)避免使用組件繼承這一點(diǎn)。
在 TypeScript 里面,類聲明的順序是很重要的。如果一個類尚未定義,就不能引用它。
這通常不是一個問題,特別是當(dāng)你遵循一個文件一個類規(guī)則的時候。 但是有時候循環(huán)引用可能不能避免。當(dāng)一個類A 引用類 B,同時'B'引用'A'的時候,你就陷入困境了:它們中間的某一個必須要先定義。
Angular 的 forwardRef()
函數(shù)建立一個間接地引用,Angular 可以隨后解析。
這個關(guān)于父查找器的例子中全都是沒辦法打破的循環(huán)類引用。
當(dāng)一個類需要引用自身的時候,你面臨同樣的困境,就像在 AlexComponent
的 provdiers
數(shù)組中遇到的困境一樣。 該 providers
數(shù)組是一個 @Component()
裝飾器函數(shù)的一個屬性,它必須在類定義之前出現(xiàn)。
使用 forwardRef
來打破這種循環(huán):
Path:"parent-finder.component.ts (AlexComponent providers)" 。
providers: [{ provide: Parent, useExisting: forwardRef(() => AlexComponent) }],
更多建議: