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