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