什么是依賴(lài)性注入?
依賴(lài)性注入( Dependency Injection )其實(shí)不是 Angular 獨(dú)有的概念,這是一個(gè)已經(jīng)存在很長(zhǎng)時(shí)間的設(shè)計(jì)模式,也可以叫做控制反轉(zhuǎn) ( Inverse of Control )。我們從下面這個(gè)簡(jiǎn)單的代碼片段入手來(lái)看看什么是依賴(lài)性注入以及為什么要使用依賴(lài)性注入。
class Person {
constructor() {
this.address = new Address('北京', '北京', '朝陽(yáng)區(qū)', 'xx街xx號(hào)');
this.id = Id.getInstance(ID_TYPES.IDCARD);
}
}
上面的代碼中,我們?cè)?Person
這個(gè)類(lèi)的構(gòu)造函數(shù)中初始化了我們構(gòu)建 Person
所需要的依賴(lài)類(lèi): Address
和 Id
,其中 Address
是個(gè)人的地址對(duì)象,而 Id
是個(gè)人身份對(duì)象。這段代碼的問(wèn)題在于除了引入了內(nèi)部所需的依賴(lài)之外, 它知道了這些依賴(lài)創(chuàng)建的細(xì)節(jié) ,比如它知道 Address
的構(gòu)造函數(shù)需要的參數(shù)(省、市、區(qū)和街道地址)和這些參數(shù)的順序,它還知道 Id
的工廠方法和其參數(shù)(取得身份證類(lèi)型的 Id
)。
但這樣做的問(wèn)題究竟是什么呢?首先這樣的代碼是非常難以進(jìn)行單元測(cè)試的,因?yàn)樵跍y(cè)試的時(shí)候我們往往需要構(gòu)造一些不同的測(cè)試場(chǎng)景(比如我們想傳入護(hù)照類(lèi)型的 Id
),但這種寫(xiě)法導(dǎo)致你沒(méi)辦法改變其行為。其次,我們?cè)诖a的可維護(hù)性和擴(kuò)展性方面有了很大的障礙,設(shè)想一下如果我們改變了 Address
的構(gòu)造函數(shù)或 Id
的工廠方法的話(huà),我們不得不去更改 Person
類(lèi)。一個(gè)類(lèi)還好,但如果幾十個(gè)類(lèi)都依賴(lài) Address
或 Person
的話(huà),這會(huì)造成多大的麻煩?
那么解決的方法呢?也很簡(jiǎn)單,那就是我們把 Person
的構(gòu)造改造一下:
class Person {
constructor(address, id) {
this.address = address;
this.id = id;
}
}
我們?cè)跇?gòu)造中接受已經(jīng)創(chuàng)建的 Address
和 Id
對(duì)象,這樣在這段代碼中就沒(méi)有任何關(guān)于它們的具體實(shí)現(xiàn)了。換句話(huà)說(shuō),我們把創(chuàng)建這些依賴(lài)性的職責(zé)向上一級(jí)傳遞了出去(噗~~推卸責(zé)任啊)。現(xiàn)在我們?cè)谏a(chǎn)代碼中可以這樣構(gòu)造 Person
:
const person = new Person(
new Address('北京', '北京', '朝陽(yáng)區(qū)', 'xx街xx號(hào)'),
Id.getInstance(ID_TYPES.IDCARD)
);
而在測(cè)試時(shí),可以方便的構(gòu)造各種場(chǎng)景,比如我們將地區(qū)改為遼寧:
const person = new Person(
new Address('遼寧', '沈陽(yáng)', '和平區(qū)', 'xx街xx號(hào)'),
Id.getInstance(ID_TYPES.PASSPORT)
);
其實(shí)這就是依賴(lài)性注入了,這個(gè)概念是不是很簡(jiǎn)單?但有的同學(xué)問(wèn)了,那上一級(jí)要是單元測(cè)試不還是有問(wèn)題嗎?是的,如果上一級(jí)需要測(cè)試,就得『推卸責(zé)任』到再上一級(jí)了。這樣一級(jí)一級(jí)的最后會(huì)推到最終的入口函數(shù),但這也不是辦法啊,而且靠人工維護(hù)也很容易出錯(cuò),這時(shí)候就需要有一個(gè)依賴(lài)性注入的框架來(lái)解決了,這種框架一般叫做 DI 框架或者 IoC 框架。這種框架對(duì)于熟悉 Java 和 .Net 的同學(xué)不會(huì)陌生,鼎鼎大名的 Spring 最初就是一個(gè)這樣的框架,當(dāng)然現(xiàn)在功能豐富多了,遠(yuǎn)不止這個(gè)功能了。
Angular 中的依賴(lài)性注入框架
Angular 中的依賴(lài)性注入框架主要包含下面幾個(gè)角色:
- Injector(注入者):使用 Injector 提供的 API 創(chuàng)建依賴(lài)的實(shí)例
- Provider(提供者):Provider 告訴 Injector 怎樣 創(chuàng)建實(shí)例(比如我們上面提到的是通過(guò)某個(gè)構(gòu)造函數(shù)還是工廠類(lèi)創(chuàng)建等等)。Provider 接受一個(gè)令牌,然后把令牌映射到一個(gè)用于構(gòu)建目標(biāo)對(duì)象的工廠函數(shù)。
- Dependency(依賴(lài)):依賴(lài)是一種 類(lèi)型 ,這個(gè)類(lèi)型就是我們要?jiǎng)?chuàng)建的對(duì)象的類(lèi)型。

可能看到這里還是有些云里霧里,沒(méi)關(guān)系,我們還是用例子來(lái)說(shuō)明:
import { ReflectiveInjector } from '@angular/core';
const injector = RelfectiveInjector.resolveAndCreate([
// providers 數(shù)組定義了多個(gè)提供者,provide 屬性定義令牌
// useXXX 定義怎樣創(chuàng)建的方法
{ provide: Person, useClass: Person },
{ provide: Address, useFactory: () => {
if(env.testing)
return new Address('遼寧', '沈陽(yáng)', '和平區(qū)', 'xx街xx號(hào)');
return new Address('北京', '北京', '朝陽(yáng)區(qū)', 'xx街xx號(hào)');
}
},
{ provide: Id, useFactory: (type) => {
if(type === ID_TYPES.PASSPORT)
return Id.getInstance(ID_TYPES.PASSPORT, someparam);
if(type === ID_TYPES.IDCARD)
return Id.getInstance(ID_TYPES.IDCARD);
return Id.getDefaultInstance();
}
}
]);
class Person {
// 通過(guò) @Inject 修飾器告訴 DI 這個(gè)參數(shù)需要什么樣類(lèi)型的對(duì)象
// 請(qǐng)?jiān)?injector 中幫我找到并注入到對(duì)應(yīng)參數(shù)中
constructor(@Inject(Address) address, @Inject(Id) id) {
// 省略
}
}
// 通過(guò) injector 得到對(duì)象
const person = injector.get(Person);
上述代碼中,Angular 提供了 RelfectiveInjector
來(lái)解析和創(chuàng)建依賴(lài)的對(duì)象,你可以看到我們把這個(gè)應(yīng)用中需要的 Person
、 Id
和 Address
都放在里面了。誰(shuí)需要這些對(duì)象就可以向 injector 請(qǐng)求,比如: injector.get(Person)
,當(dāng)然也可以 injector.get(Address)
等等。可以把它理解成一個(gè)依賴(lài)性的池子,想要什么就取就好了。
但是問(wèn)題來(lái)了,首先 injector 怎么知道如何創(chuàng)建你需要的對(duì)象呢?這個(gè)是靠 Provider 定義的,在剛剛的 RelfectiveInjector.resolveAndCreate()
中我們發(fā)現(xiàn)它是接受一個(gè)數(shù)組作為參數(shù),這個(gè)數(shù)組就是一個(gè) Provider 的數(shù)組。Provider 最常見(jiàn)的屬性有兩個(gè)。第一個(gè)是 provide
,這個(gè)屬性其實(shí)定義的是令牌,令牌的作用是讓框架知道你要找的依賴(lài)是哪個(gè)然后就可以在 useXXX
這個(gè)屬性定義的構(gòu)建方式中將你需要的對(duì)象構(gòu)建出來(lái)了。
那么 constructor(@Inject(Address) address, @Inject(Id) id)
這句怎么理解呢?由于我們?cè)?const person = injector.get(Person);
想取得 Person ,但 Person 又需要兩個(gè)依賴(lài)參數(shù): address 和 id 。 @Inject(Address) address
是告訴框架我需要的是一個(gè)令牌為 Address 的對(duì)象,這樣框架就又到 injector 中尋找令牌為 Address 對(duì)應(yīng)的工廠函數(shù),通過(guò)工廠函數(shù)構(gòu)造好對(duì)象后又把對(duì)象賦值到 address 。
由于這里我們是用對(duì)象的類(lèi)型來(lái)做令牌,上面的注入代碼也可以寫(xiě)成下面的樣子。利用 Typescript 的類(lèi)型定義,框架看到有依賴(lài)的參數(shù)就會(huì)去 Injector 中尋找令牌為該類(lèi)型的工廠函數(shù)。
class Person {
constructor(address: Address, id: Id) {
// 省略
}
}
而對(duì)于令牌為類(lèi)型的并且是 useClass
的這種形式,由于前后都一樣,對(duì)于這種 Provider 我們有一個(gè)語(yǔ)法糖:可以直接寫(xiě)成 { Person }
,而不用完整的寫(xiě)成 { provide: Person, useClass: Person }
這種形式。當(dāng)然還要注意 Token 不一定非得是某個(gè)類(lèi)的類(lèi)型,也可以是字符串, Angular 中還有 InjectionToken
用于創(chuàng)建一個(gè)可以避免重名的 Token。
那么其實(shí)除了 useClass
和 useFactory
,我們還可以使用 useValue
來(lái)提供一些簡(jiǎn)單數(shù)據(jù)結(jié)構(gòu),比如我們可能希望把系統(tǒng)的 API 基礎(chǔ)信息配置通過(guò)這種形式讓所有想調(diào)用 API 的類(lèi)都可以注入。如下面的例子中,基礎(chǔ)配置就是一個(gè)簡(jiǎn)單的對(duì)象,里面有多個(gè)屬性,這種情況用 useValue
即可。
{
provide: 'BASE_CONFIG',
useValue: {
uri: 'https://dev.local/1.1',
apiSecret: 'blablabla',
apiKey: 'nahnahnah'
}
}
依賴(lài)性注入進(jìn)階
可能你注意到,上面提到的依賴(lài)性注入有一個(gè)特點(diǎn),那就是需要注入的參數(shù)如果在 Injector 中找不到對(duì)應(yīng)的依賴(lài),那么就會(huì)發(fā)生異常了。但確實(shí)有些時(shí)候我們是需要這樣的特性:該依賴(lài)是可選的,如果有我們就這么做,如果沒(méi)有就那樣做。遇到這種情況怎么辦呢?
Angular 提供了一個(gè)非常貼心的 @Optional
修飾器,這個(gè)修飾器用來(lái)告訴框架后面的參數(shù)需要一個(gè)可選的依賴(lài)。
constructor(@Optional(ThirdPartyLibrary) lib) {
if (!lib) {
// 如果該依賴(lài)不存在的情況
}
}
需要注意的是,Angular 的 DI 框架創(chuàng)建的對(duì)象都是單件( Singleton )的,那么如果我們需要每次都創(chuàng)建一個(gè)新對(duì)象怎么破呢?我們有兩個(gè)選擇,第一種:在 Provider 中返回工廠而不是對(duì)象,像下面例子這樣:
{
provide: Address,
useFactory: () => {
// 注意:這里返回的是工廠,而不是對(duì)象
return () => {
if(env.testing)
return new Address('遼寧', '沈陽(yáng)', '和平區(qū)', 'xx街xx號(hào)');
return new Address('北京', '北京', '朝陽(yáng)區(qū)', 'xx街xx號(hào)');
}
}
}
第二種:我們創(chuàng)建一個(gè) child injector
(子注入者): Injector.resolveAndCreateChild()
const injector = ReflectiveInjector.resolveAndCreate([Person]);
const childInjector = injector.resolveAndCreateChild([Person]);
// 此時(shí)父 Injector 和子 Injector 得到的 Person 對(duì)象是不同的
injector.get(Person) !== childInjector.get(Person);
而且子 Injector 還有一個(gè)特性:如果在 childInjector
中找不到令牌對(duì)應(yīng)的工廠,它會(huì)去父 Injector 中尋找。換句話(huà)說(shuō),這父子關(guān)系(多重的)是構(gòu)成了一棵依賴(lài)樹(shù),框架會(huì)從最下面的子 Injector 開(kāi)始尋找,一直找到最上面的父 Injector。看到這里相信你就知道為什么父組件聲明的 providers 對(duì)于子組件是可見(jiàn)的,因?yàn)樽咏M件中在自己 constructor 中如果發(fā)現(xiàn)有找不到的依賴(lài)就會(huì)到父組件中去找。
在實(shí)際的 Angular 應(yīng)用中我們其實(shí)很少會(huì)直接顯式使用 Injector 去完成注入,而是在對(duì)應(yīng)的模塊、組件等的元數(shù)據(jù)中提供 providers 即可,這是由于 Angular 框架幫我們完成了這部分代碼,它們其實(shí)在元數(shù)據(jù)配置后由框架放入 Injector 中了。
有問(wèn)題的童鞋可以加入我的小密圈討論: http://t.xiaomiquan.com/jayRnaQ (該鏈接7天內(nèi)(5月14日前)有效)
我的 《Angular 從零到一》紙書(shū)出版了,歡迎大家圍觀、訂購(gòu)、提出寶貴意見(jiàn)。
下面是書(shū)籍的內(nèi)容簡(jiǎn)介:
本書(shū)系統(tǒng)介紹Angular的基礎(chǔ)知識(shí)與開(kāi)發(fā)技巧,可幫助前端開(kāi)發(fā)者快速入門(mén)。共有9章,第1章介紹Angular的基本概念,第2~7章從零開(kāi)始搭建一個(gè)待辦事項(xiàng)應(yīng)用,然后逐步增加功能,如增加登錄驗(yàn)證、將應(yīng)用模塊化、多用戶(hù)版本的實(shí)現(xiàn)、使用第三方樣式庫(kù)、動(dòng)態(tài)效果制作等。第8章介紹響應(yīng)式編程的概念和Rx在Angular中的應(yīng)用。第9章介紹在React中非常流行的Redux狀態(tài)管理機(jī)制,這種機(jī)制的引入可以讓代碼和邏輯隔離得更好,在團(tuán)隊(duì)工作中強(qiáng)烈建議采用這種方案。本書(shū)不僅講解Angular的基本概念和最佳實(shí)踐,而且分享了作者解決問(wèn)題的過(guò)程和邏輯,講解細(xì)膩,風(fēng)趣幽默,適合有面向?qū)ο缶幊袒A(chǔ)的讀者閱讀。
慕課網(wǎng) Angular 視頻課上線: http://coding.imooc.com/class/123.html?mc_marking=1fdb7649e8a8143e8b81e221f9621c4a&mc_channel=banner
京東鏈接:https://item.m.jd.com/product/12059091.html?from=singlemessage&isappinstalled=0