1. 使用DI
依賴注入是一個很重要的程序設計模式。 Angular 有自己的依賴注入框架,離開了它,我們幾乎沒法構建 Angular 應用。它使用得非常廣泛,以至於幾乎每個人都會把它簡稱為 DI。
我們來看一個簡單的例子:
export class Animal { dogs; constructor() { var dog = new Dog(); } } |
我們的Animal在構造函數中手工創建所需的每樣東西。問題在於,我們這個 Animal
類過於脆弱、缺乏彈性並且難以測試。
當我們的Animal 類需要一個 Dog,沒有去請求一個現成的實例, 而是在構造函數中用具體的 Dog類新創建了一份只供自己用的副本。
如果 Dog類升級了,並且它的構造函數要求傳入一個參數了,該怎么辦? 我們這個Animal類就被破壞了,而且直到我們把創建引擎的代碼重寫為 Dog= new Dog(theNewParameter) 之前,它都是壞的。但是當Dog類的定義發生變化時,我們就不得不在乎了,Animal類也不得不跟着改變。 這就會讓Animal類過於脆弱。
現在,每個Animal都有自己獨特的Dog。他無法被其他Animal共享。我們的Animal缺乏必要的彈性,無法共享給其他的Animal類消費。
我們該如何讓 Animal更強壯、有彈性以及可測試?
答案超級簡單。我們把Animal的構造函數改造成使用 DI 的版本:
export class Animal { dogs; constructor(private dog:Dog) { } } |
發生了什么?我們把依賴的定義移到了構造函數中。 我們的Animal類不再創建Dog, 它僅僅“消費”它們。如果有人擴展了Dog類,那就不再是Animal類的煩惱了。
2. Angular DI
Angular 自帶了它自己的依賴注入框架。此框架也能被當做獨立模塊用於其它應用和框架中。
2.1 注入器樹
Angular 有一個多級依賴注入系統。實際上,應用程序中有一個與組件樹平行的注入器樹,我們可以在組件樹中的任何級別上重新配置注入器。常見的注入器數的形式如下:
當一個底層的組件申請獲得一個依賴時, Angular 先嘗試用該組件自己的注入器來滿足它。 如果該組件的注入器沒有找到對應的提供商,它就把這個申請轉給它父組件的注入器來處理。 如果那個注入器也無法滿足這個申請,它就繼續轉給 它的父組件的注入器。 這個申請繼續往上冒泡——直到我們找到了一個能處理此申請的注入器或者超出了組件樹中的祖先位置為止。 如果超出了組件樹中的祖先還未找到, Angular 就會拋出一個錯誤。
2.2 實現原理
Angular給依賴注入器提供令牌來獲取服務。通常在構造函數里面,為參數指定類型,讓 Angular 來處理依賴注入。該參數類型就是依賴注入器所需的 令牌 。 Angular 把該令牌傳給注入器,然后把得到的結果賦給參數。
注入器從哪兒得到的依賴? 它可能在自己內部容器里已經有該依賴了。 如果它沒有,也能在 提供商 的幫助下新建一個。 提供商 就是一個用於交付服務的配方,它被關聯到一個令牌。Angular會根據該令牌根據供應商創建一個服務結果返回,並將其保存在注入器內部供以后調用。
2.3 令牌
當我們為注入器注冊一個提供商時,實際上是把這個提供商和一個 DI 令牌關聯起來了。 注入器維護一個內部的 令牌 - 提供商 映射表,這個映射表會在請求一個依賴時被引用到。令牌就是這個映射表中的鍵值 key 。
2.3.1 類依賴
一般情況下,依賴值都是一個類 實例 ,並且類的類型是它自己的查找鍵值。 這種情況下,我們實際上是直接從注入器中以 類型作為令牌,來獲取一個 實例。
寫一個需要基於類的依賴注入的構造函數對我們來說是很幸運的。我們只要以 類為類型,定義一個構造函數參數, Angular 就會知道把跟 類令牌關聯的服務注入進來,大多數依賴值都是以類的形式提供的。
例如,其中Animal依賴Dog類,在構造函數中提供Dog類型,就可以依賴注入對應的Dog類實例。
export class Animal { dogs; constructor(private dog:Dog) { } } |
請注意,TypeScript 接口不是一個有效的令牌。
2.3.2 非類依賴
如果依賴值不是一個類呢?有時候我們想要注入的東西是一個字符串,函數或者對象。
應用程序經常為很多很小的因素 ( 比如應用程序的標題,或者一個網絡 API 終點的地址 ) 定義配置對象,但是這些配置對象不總是類的實例。
但是這種情況下我們要把什么用作令牌呢? 解決方案是定義和使用一個 OpaqueToken。定義方式類似於這樣:
import { OpaqueToken } from '@angular/core'; export let APP_CONFIG = new OpaqueToken('app.config'); |
我們使用這個 OpaqueToken
對象注冊依賴的提供商:
providers: [{ provide: APP_CONFIG, useValue: HERO_DI_CONFIG }] |
現在,在 @Inject
的幫助下,我們可以把這個配置對象注入到任何需要它的構造函數中:
constructor(@Inject(APP_CONFIG) config: AppConfig) { this.title = config.title; } |
雖然 ConfigAppConfig
接口在依賴注入過程中沒有任何作用,但它為該類中的配置對象提供了強類型信息。
2.4 提供商
提供商 提供 依賴注入的一個運行時版本。 注入器依靠 提供商們 來創建服務的實例,它會被注入器注入到組件或其它服務中。
Angular中使用provide對象來作為提供商,該 provide 對象需要一個令牌 和一個定義對象,該令牌通常是一個類,但並非一定是。
2.4.1 userValue-值提供商
該定義對象有一個主屬性 ( 即userValue) ,用來標識該提供商會如何新建和返回依賴。
把一個固定的值 ,也就是該提供商可以將其作為依賴對象返回的值,賦給 userValue 屬性。
通常使用該技巧來進行運行期常量設置 ,比如網站的基礎地址和功能標志等。 我們在OpaqueToken中已經見識了一個例子,我們為APP_CONFIG提供了一個常量作為定義對象。
{ provide: APP_CONFIG, useValue: HERO_DI_CONFIG } |
一個值提供商的值必須要立即定義。不能事后再定義它的值。很顯然,標題字符串是立刻可用的。
2.4.2 useClass -類提供商
userClass 提供商創建並返回一個指定類的新實例。使用該技術來為公共或默認類 提供備選實現。一般來說,被新建的類同時也是該提供商的注入令牌,例如一個提供日志服務的提供商
{ provide: LoggerService, useClass:LoggerService } |
我們在依賴注入LoggerService時,會根據類LoggerService來創建一個默認的示例作為結果返回。
當依賴注入的類有其他類型依賴的情況下,例如LoggerService依賴於用戶信息,我們同樣用構造函數注入模式,來添加一個帶有 LoggerService參數的構造函數。這種情況下就需要用到Injectable注解。
@Injectable() 標志着一個類可以被一個注入器實例化。當我們的LoggerService服務有了一個注入的依賴,我們需要使用@Injectable()來標識LoggerService,這樣Angular 就可以使用構造函數參數的元數據來注入一個 用戶服務
。
2.4.3 useExisting - 別名提供商
useExisting ,提供商可以把一個令牌映射到另一個令牌上。實際上,第一個令牌是第二個令牌所對應的服務的一個別名,創造了訪問同一個服務對象的兩種方法 。
{ provide: MinimalLogger, useExisting:LoggerService } |
通過使用別名接口來把一個 API 變窄,是一個很重要的該技巧的使用例子。我們在這里就是為了這個目的使用的別名。 想象一下如果 LoggerService 有個很大的 API 接口 ( 雖然它其實只有三個方法,一個屬性 ) ,通過使用 MinimalLogger 類 - 接口別名,就能成功的把這個 API 接口縮小到只暴露兩個成員:
export abstract class MinimalLogger { logInfo: (msg: string) => void; logs: string[]; } |
2.4.4 useFactory工廠提供商
useFactory 提供商通過調用工廠函數來新建一個依賴對象,主要用來創建一個擁有參數的對象來作為提供者。
使用這項技術,可以用包含了一些 依賴服務和本地狀態 輸入的工廠函數來 建立一個依賴對象,該依賴對象不一定是一個類實例,它可以是任何東西。例如
export function factory(level){ return new Logger(level) } { provide: RUNNERS_UP, useFactory: factory, deps: [2] } |
2.5 配置注入器。
一般來說輸入器的位置有兩種,一種是在NgModule中注入,一種是在Component中注入。兩種類型都是在元數據中的providers數組中注入,區別在在生效的范圍不同,Component中注入只在當前組件以及子組件中生效。例如
Providers:[ UserService, { provide: Logger, useClass: EvenBetterLogger } ] |
其中UserService即是類提供商的簡寫
{ provide: UserService, useClass: UserService} |
2.6 使用DI
我們知道了如何配置服務提供商,現在我們來了解一下如何使用。
2.6.1 構造函數
通常情況下我們使用構造函數參數來注入對應的服務。一般來講主要存在兩種情況。
首先,是類型作為令牌的依賴注入,這種情況下,可以直接使用構造函數中的參數類型進行依賴注入,例如在Animal中使用Dog類型
export class Animal { dogs; constructor(private dog:Dog) { } } |
其次,可以使用@Inject(‘token’)的形式注入令牌的類型的對象或者服務,例如我們注入值類型的配置對象
constructor(@Inject(APP_CONFIG) config: AppConfig) { this.title = config.title; } |
2.6.2 獲取父組件
通常來說獲取父組件就是獲取一個已經存在的組件類型,父組件必須通過提供一個與別名提供者來實現,例如
providers: [{provide: Parent, useExisting: forwardRef(() => ParentComponent) }] |
Parent 是該提供商的令牌, ParentComponent就是該別名提供商的類型,將該類型提供商注入到父組件的注入器中,則子組件可以使用Parent令牌作為構造函數參數類型來注入該服務,獲取ParentComponent。 ParentComponent引用了自身,造成循環引用,必須使用 前向引用forwardRef 打破了該循環,查找當前或者父級的提供商。
2.6.3 跳過自身與可選
當我們不想從當前元素獲取依賴的時候,可以使用@SkipSelf(),這樣注入器從一個在自己 上一級 的組件開始搜索一個 Parent
依賴。同時,當無法確保依賴是否存在的情況下,而又為了避免拋出找不到依賴的錯誤情況,可以使用@Optional()注解,這樣該依賴是可選的,例如引入父組件的構造函數可以寫成如下的格式:
constructor( @SkipSelf() @Optional() public parent: Parent ) { } |