依賴提供者會(huì)使用 DI 令牌來配置注入器,注入器會(huì)用它來提供這個(gè)依賴值的具體的、運(yùn)行時(shí)版本。 注入器依靠 "提供者配置" 來創(chuàng)建依賴的實(shí)例,并把該實(shí)例注入到組件、指令、管道和其它服務(wù)中。
你必須使用提供者來配置注入器,否則注入器就無法知道如何創(chuàng)建此依賴。 注入器創(chuàng)建服務(wù)實(shí)例的最簡單方法,就是用這個(gè)服務(wù)類本身來創(chuàng)建它。 如果你把服務(wù)類作為此服務(wù)的 DI 令牌,注入器的默認(rèn)行為就是 new 出這個(gè)類實(shí)例。
在下面這個(gè)典型的例子中,Logger 類自身提供了
Logger` 的實(shí)例。
providers: [Logger]
不過,你也可以用一個(gè)替代提供者來配置注入器,這樣就可以指定另一些同樣能提供日志功能的對(duì)象。 比如:
Logger
的對(duì)象。logger
。
類提供者的語法實(shí)際上是一種簡寫形式,它會(huì)擴(kuò)展成一個(gè)由 Provider
接口定義的提供者配置對(duì)象。 下面的代碼片段展示了 providers
中給出的類會(huì)如何擴(kuò)展成完整的提供者配置對(duì)象。
providers: [Logger]
[{ provide: Logger, useClass: Logger }]
擴(kuò)展的提供者配置是一個(gè)具有兩個(gè)屬性的對(duì)象字面量。
provide
屬性存有令牌,它作為一個(gè) key
,在定位依賴值和配置注入器時(shí)使用。key
可以是 useClass
—— 就像這個(gè)例子中一樣。 也可以是 useExisting
、useValue
或 useFactory
。 每一個(gè) key
都用于提供一種不同類型的依賴,我們稍后會(huì)討論。
不同的類都可用于提供相同的服務(wù)。 比如,下面的代碼告訴注入器,當(dāng)組件使用 Logger
令牌請(qǐng)求日志對(duì)象時(shí),給它返回一個(gè) BetterLogger
實(shí)例。
[{ provide: Logger, useClass: BetterLogger }]
擴(kuò)展的提供者配置是一個(gè)具有兩個(gè)屬性的對(duì)象字面量。
provide
屬性存有令牌,它作為一個(gè) key
,在定位依賴值和配置注入器時(shí)使用。key
可以是 useClass
—— 就像這個(gè)例子中一樣。 也可以是 useExisting
、useValue
或 useFactory
。 每一個(gè) key
都用于提供一種不同類型的依賴,我們稍后會(huì)討論。
不同的類都可用于提供相同的服務(wù)。 比如,下面的代碼告訴注入器,當(dāng)組件使用 Logger
令牌請(qǐng)求日志對(duì)象時(shí),給它返回一個(gè) BetterLogger
實(shí)例。
[{ provide: Logger, useClass: BetterLogger }]
另一個(gè)類 EvenBetterLogger
可能要在日志信息里顯示用戶名。 這個(gè) logger
要從注入的 UserService
實(shí)例中來獲取該用戶。
@Injectable()
export class EvenBetterLogger extends Logger {
constructor(private userService: UserService) { super(); }
log(message: string) {
let name = this.userService.user.name;
super.log(`Message to ${name}: ${message}`);
}
}
注入器需要提供這個(gè)新的日志服務(wù)以及該服務(wù)所依賴的 UserService
對(duì)象。 使用 useClass
作為提供者定義對(duì)象的 key
,來配置一個(gè) logger
的替代品,比如 BetterLogger
。 下面的數(shù)組同時(shí)在父模塊和組件的 providers
元數(shù)據(jù)選項(xiàng)中指定了這些提供者。
[ UserService,
{ provide: Logger, useClass: EvenBetterLogger }]
假設(shè)老的組件依賴于 OldLogger
類。OldLogger
和 NewLogger
的接口相同,但是由于某種原因,我們沒法修改老的組件來使用 NewLogger
。
當(dāng)老的組件要使用 OldLogger
記錄信息時(shí),你可能希望改用 NewLogger
的單例來處理它。 在這種情況下,無論某個(gè)組件請(qǐng)求老的 logger
還是新的 logger
,依賴注入器都應(yīng)該注入這個(gè) NewLogger
的單例。 也就是說 OldLogger
應(yīng)該是 NewLogger
的別名。
如果你試圖用 useClass
為 OldLogger
指定一個(gè)別名 NewLogger
,就會(huì)在應(yīng)用中得到 NewLogger
的兩個(gè)不同的實(shí)例。
[ NewLogger,
// Not aliased! Creates two instances of `NewLogger`
{ provide: OldLogger, useClass: NewLogger}]
要確保只有一個(gè) NewLogger
實(shí)例,就要用 useExisting
來為 OldLogger
指定別名。
[ NewLogger,
// Alias OldLogger w/ reference to NewLogger
{ provide: OldLogger, useExisting: NewLogger}]
有時(shí)候,提供一個(gè)現(xiàn)成的對(duì)象會(huì)比要求注入器從類去創(chuàng)建更簡單一些。 如果要注入一個(gè)你已經(jīng)創(chuàng)建過的對(duì)象,請(qǐng)使用 useValue
選項(xiàng)來配置該注入器。
下面的代碼定義了一個(gè)變量,用來創(chuàng)建這樣一個(gè)能扮演 logger
角色的對(duì)象。
// An object in the shape of the logger service
function silentLoggerFn() {}
export const SilentLogger = {
logs: ['Silent logger says "Shhhhh!". Provided via "useValue"'],
log: silentLoggerFn
};
下面的提供者定義對(duì)象使用 useValue
作為 key
來把該變量與 Logger
令牌關(guān)聯(lián)起來。
[{ provide: Logger, useValue: SilentLogger }]
并非所有的依賴都是類。 有時(shí)候你會(huì)希望注入字符串、函數(shù)或?qū)ο蟆?/p>
應(yīng)用通常會(huì)用大量的小型參數(shù)來定義配置對(duì)象,比如應(yīng)用的標(biāo)題或 Web API 端點(diǎn)的地址。 這些配置對(duì)象不一定總是類的實(shí)例。 它們還可能是對(duì)象字面量,如下例所示。
Path:"src/app/app.config.ts (excerpt)" 。
export const HERO_DI_CONFIG: AppConfig = {
apiEndpoint: 'api.heroes.com',
title: 'Dependency Injection'
};
TypeScript 接口不是有效的令牌
HERO_DI_CONFIG
常量滿足 AppConfig
接口的要求。 不幸的是,你不能用 TypeScript 的接口作為令牌。 在 TypeScript 中,接口是一個(gè)設(shè)計(jì)期的概念,無法用作 DI
框架在運(yùn)行期所需的令牌。
// FAIL! Can't use interface as provider token
[{ provide: AppConfig, useValue: HERO_DI_CONFIG })]
// FAIL! Can't inject using the interface as the parameter type
constructor(private config: AppConfig){ }
如果你曾經(jīng)在強(qiáng)類型語言中使用過依賴注入功能,這一點(diǎn)可能看起來有點(diǎn)奇怪,那些語言都優(yōu)先使用接口作為查找依賴的
key
。 不過,JavaScript 沒有接口,所以,當(dāng) TypeScript 轉(zhuǎn)譯成 JavaScript 時(shí),接口也就消失了。 在運(yùn)行期間,沒有留下任何可供 Angular 進(jìn)行查找的接口類型信息。
替代方案之一是以類似于 AppModule
的方式,在 NgModule
中提供并注入這個(gè)配置對(duì)象。
Path:"src/app/app.module.ts (providers)" 。
providers: [
UserService,
{ provide: APP_CONFIG, useValue: HERO_DI_CONFIG }
],
另一個(gè)為非類依賴選擇提供者令牌的解決方案是定義并使用 InjectionToken
對(duì)象。 下面的例子展示了如何定義那樣一個(gè)令牌。
Path:"src/app/app.config.ts" 。
import { InjectionToken } from '@angular/core';
export const APP_CONFIG = new InjectionToken<AppConfig>('app.config');
雖然類型參數(shù)在這里是可選的,不過還是能把此依賴的類型信息傳達(dá)給開發(fā)人員和開發(fā)工具。 這個(gè)令牌的描述則是開發(fā)人員的另一個(gè)助力。
使用 InjectionToken
對(duì)象注冊(cè)依賴提供者:
Path:"src/app/app.component.ts" 。
constructor(@Inject(APP_CONFIG) config: AppConfig) {
this.title = config.title;
}
雖然
AppConfig
接口在依賴注入時(shí)沒有任何作用,但它可以為該組件類中的這個(gè)配置對(duì)象指定類型信息。
有時(shí)候你需要?jiǎng)討B(tài)創(chuàng)建依賴值,創(chuàng)建時(shí)需要的信息你要等運(yùn)行期間才能拿到。 比如,你可能需要某個(gè)在瀏覽器會(huì)話過程中會(huì)被反復(fù)修改的信息,而且這個(gè)可注入服務(wù)還不能獨(dú)立訪問這個(gè)信息的源頭。
這種情況下,你可以使用工廠提供者。 當(dāng)需要從第三方庫創(chuàng)建依賴項(xiàng)實(shí)例時(shí),工廠提供者也很有用,因?yàn)榈谌綆觳皇菫?DI
而設(shè)計(jì)的。
比如,假設(shè) HeroService
必須對(duì)普通用戶隱藏秘密英雄,只有得到授權(quán)的用戶才能看到他們。
像 EvenBetterLogger
一樣,HeroService
需要知道該用戶是否有權(quán)查看秘密英雄。 而認(rèn)證信息可能會(huì)在應(yīng)用的單個(gè)會(huì)話中發(fā)生變化,比如你改用另一個(gè)用戶登錄。
假設(shè)你不希望直接把 UserService
注入到 HeroService
中,因?yàn)槟悴幌M堰@個(gè)服務(wù)與那些高度敏感的信息牽扯到一起。 這樣 HeroService
就無法直接訪問到用戶信息,來決定誰有權(quán)訪問,誰沒有。
要解決這個(gè)問題,我們給 HeroService
的構(gòu)造函數(shù)一個(gè)邏輯型標(biāo)志,以控制是否顯示秘密英雄。
Path:"src/app/heroes/hero.service.ts (excerpt)" 。
constructor(
private logger: Logger,
private isAuthorized: boolean) { }
getHeroes() {
let auth = this.isAuthorized ? 'authorized ' : 'unauthorized';
this.logger.log(`Getting heroes for ${auth} user.`);
return HEROES.filter(hero => this.isAuthorized || !hero.isSecret);
}
你可以注入 Logger
但是不能注入 isAuthorized
標(biāo)志。不過你可以改用工廠提供者來為 HeroServic
e 創(chuàng)建一個(gè)新的 logger
實(shí)例。
工廠提供者需要一個(gè)工廠函數(shù)。
Path:"src/app/heroes/hero.service.provider.ts (excerpt)" 。
let heroServiceFactory = (logger: Logger, userService: UserService) => {
return new HeroService(logger, userService.user.isAuthorized);
};
雖然 HeroService
不能訪問 UserService
,但是工廠函數(shù)可以。 你把 Logger
和 UserService
注入到了工廠提供者中,并讓注入器把它們傳給這個(gè)工廠函數(shù)。
Path:"src/app/heroes/hero.service.provider.ts (excerpt)" 。
export let heroServiceProvider =
{ provide: HeroService,
useFactory: heroServiceFactory,
deps: [Logger, UserService]
};
useFactory
字段告訴 Angular 該提供者是一個(gè)工廠函數(shù),該函數(shù)的實(shí)現(xiàn)代碼是 heroServiceFactory
。deps
屬性是一個(gè)提供者令牌數(shù)組。 Logger
和 UserService
類作為它們自己的類提供者令牌使用。 注入器解析這些令牌,并把與之對(duì)應(yīng)的服務(wù)注入到相應(yīng)的工廠函數(shù)參數(shù)表中。
注意,你把這個(gè)工廠提供者保存到了一個(gè)導(dǎo)出的變量 heroServiceProvider
中。 這個(gè)額外的步驟讓工廠提供者可被復(fù)用。 你可以在任何需要它的地方用這個(gè)變量來配置 HeroService
的提供者。 在這個(gè)例子中,你只在 HeroesComponent
中用到了它。你在該組件元數(shù)據(jù)的 providers
數(shù)組中用 heroServiceProvider
替換了 HeroService
。
下面并列顯示了新舊實(shí)現(xiàn)。
import { Component } from '@angular/core';
import { heroServiceProvider } from './hero.service.provider';
@Component({
selector: 'app-heroes',
providers: [ heroServiceProvider ],
template: `
<h2>Heroes</h2>
<app-hero-list></app-hero-list>
`
})
export class HeroesComponent { }
import { Component } from '@angular/core';
import { HeroService } from './hero.service';
@Component({
selector: 'app-heroes',
providers: [ HeroService ],
template: `
<h2>Heroes</h2>
<app-hero-list></app-hero-list>
`
})
export class HeroesComponent { }
Angular 提供了一些內(nèi)置的注入令牌常量,你可以用它們來自定義系統(tǒng)的多種行為。
比如,你可以使用下列內(nèi)置令牌來切入 Angular 框架的啟動(dòng)和初始化過程。 提供者對(duì)象可以把任何一個(gè)注入令牌與一個(gè)或多個(gè)用來執(zhí)行應(yīng)用初始化操作的回調(diào)函數(shù)關(guān)聯(lián)起來。
PLATFORM_INITIALIZER
:平臺(tái)初始化之后調(diào)用的回調(diào)函數(shù)。APP_BOOTSTRAP_LISTENER
:每個(gè)啟動(dòng)組件啟動(dòng)完成之后調(diào)用的回調(diào)函數(shù)。這個(gè)處理器函數(shù)會(huì)收到這個(gè)啟動(dòng)組件的 ComponentRef
實(shí)例。APP_INITIALIZER
:應(yīng)用初始化之前調(diào)用的回調(diào)函數(shù)。注冊(cè)的所有初始化器都可以(可選地)返回一個(gè) Promise
。所有返回 Promise
的初始化函數(shù)都必須在應(yīng)用啟動(dòng)之前解析完。如果任何一個(gè)初始化器失敗了,該應(yīng)用就不會(huì)繼續(xù)啟動(dòng)。
該提供者對(duì)象還有第三個(gè)選項(xiàng) multi: true
,把它和 APP_INITIALIZER
一起使用可以為特定的事件注冊(cè)多個(gè)處理器。
比如,當(dāng)啟動(dòng)應(yīng)用時(shí),你可以使用同一個(gè)令牌注冊(cè)多個(gè)初始化器。
export const APP_TOKENS = [
{ provide: PLATFORM_INITIALIZER, useFactory: platformInitialized, multi: true },
{ provide: APP_INITIALIZER, useFactory: delayBootstrapping, multi: true },
{ provide: APP_BOOTSTRAP_LISTENER, useFactory: appBootstrapped, multi: true },
];
在其它地方,多個(gè)提供者也同樣可以和單個(gè)令牌關(guān)聯(lián)起來。 比如,你可以使用內(nèi)置的 NG_VALIDATORS
令牌注冊(cè)自定義表單驗(yàn)證器,還可以在提供者定義對(duì)象中使用 multi: true
屬性來為指定的驗(yàn)證器令牌提供多個(gè)驗(yàn)證器實(shí)例。 Angular 會(huì)把你的自定義驗(yàn)證器添加到現(xiàn)有驗(yàn)證器的集合中。
路由器也同樣用多個(gè)提供者關(guān)聯(lián)到了一個(gè)令牌。 當(dāng)你在單個(gè)模塊中用 RouterModule.forRoot
和 RouterModule.forChild
提供了多組路由時(shí),ROUTES
令牌會(huì)把這些不同的路由組都合并成一個(gè)單一值。
搖樹優(yōu)化是指一個(gè)編譯器選項(xiàng),意思是把應(yīng)用中未引用過的代碼從最終生成的包中移除。 如果提供者是可搖樹優(yōu)化的,Angular 編譯器就會(huì)從最終的輸出內(nèi)容中移除應(yīng)用代碼中從未用過的服務(wù)。 這會(huì)顯著減小你的打包體積。
理想情況下,如果應(yīng)用沒有注入服務(wù),它就不應(yīng)該包含在最終輸出中。 不過,Angular 要能在構(gòu)建期間識(shí)別出該服務(wù)是否需要。 由于還可能用
injector.get(Service)
的形式直接注入服務(wù),所以 Angular 無法準(zhǔn)確識(shí)別出代碼中可能發(fā)生此注入的全部位置,因此為保險(xiǎn)起見,只能把服務(wù)包含在注入器中。 因此,在NgModule
或 組件級(jí)別提供的服務(wù)是無法被搖樹優(yōu)化掉的。
下面這個(gè)不可搖樹優(yōu)化的 Angular 提供者的例子為 NgModule
注入器配置了一個(gè)服務(wù)提供者。
Path:"src/app/tree-shaking/service-and-modules.ts" 。
import { Injectable, NgModule } from '@angular/core';
@Injectable()
export class Service {
doSomething(): void {
}
}
@NgModule({
providers: [Service],
})
export class ServiceModule {
}
你可以把該模塊導(dǎo)入到你的應(yīng)用模塊中,以便該服務(wù)可注入到你的應(yīng)用中,例子如下。
Path:"src/app/tree-shaking/app.modules.ts" 。
@NgModule({
imports: [
BrowserModule,
RouterModule.forRoot([]),
ServiceModule,
],
})
export class AppModule {
}
當(dāng)運(yùn)行 ngc
時(shí),它會(huì)把 AppModule
編譯到模塊工廠中,工廠包含該模塊及其導(dǎo)入的所有模塊中聲明的所有提供者。在運(yùn)行時(shí),該工廠會(huì)變成負(fù)責(zé)實(shí)例化所有這些服務(wù)的注入器。
這里搖樹優(yōu)化不起作用,因?yàn)?Angular 無法根據(jù)是否用到了其它代碼塊(服務(wù)類),來決定是否能排除這塊代碼(模塊工廠中的服務(wù)提供者定義)。要讓服務(wù)可以被搖樹優(yōu)化,關(guān)于如何構(gòu)建該服務(wù)實(shí)例的信息(即提供者定義),就應(yīng)該是服務(wù)類本身的一部分。
只要在服務(wù)本身的 @Injectable()
裝飾器中指定,而不是在依賴該服務(wù)的 NgModule
或組件的元數(shù)據(jù)中指定,你就可以制作一個(gè)可搖樹優(yōu)化的提供者。
下面的例子展示了與上面的 ServiceModule
例子等價(jià)的可搖樹優(yōu)化的版本。
Path:"src/app/tree-shaking/service.ts" 。
@Injectable({
providedIn: 'root',
})
export class Service {
}
該服務(wù)還可以通過配置工廠函數(shù)來實(shí)例化,如下例所示。
Path:"src/app/tree-shaking/service.0.ts" 。
@Injectable({
providedIn: 'root',
useFactory: () => new Service('dependency'),
})
export class Service {
constructor(private dep: string) {
}
}
要想覆蓋可搖樹優(yōu)化的提供者,請(qǐng)使用其它提供者來配置指定的
NgModule
或組件的注入器,只要使用@NgModule()
或@Component()
裝飾器中的providers: []
數(shù)組就可以了。
更多建議: