什么是依賴性注入?
依賴性注入( Dependency Injection )其實不是 Angular 獨有的概念,這是一個已經存在很長時間的設計模式物赶,也可以叫做控制反轉 ( Inverse of Control )室梅。我們從下面這個簡單的代碼片段入手來看看什么是依賴性注入以及為什么要使用依賴性注入。
class Person {
constructor() {
this.address = new Address('北京', '北京', '朝陽區(qū)', 'xx街xx號');
this.id = Id.getInstance(ID_TYPES.IDCARD);
}
}
上面的代碼中菱肖,我們在 Person
這個類的構造函數(shù)中初始化了我們構建 Person
所需要的依賴類: Address
和 Id
客冈,其中 Address
是個人的地址對象,而 Id
是個人身份對象稳强。這段代碼的問題在于除了引入了內部所需的依賴之外场仲, 它知道了這些依賴創(chuàng)建的細節(jié) ,比如它知道 Address
的構造函數(shù)需要的參數(shù)(省退疫、市渠缕、區(qū)和街道地址)和這些參數(shù)的順序,它還知道 Id
的工廠方法和其參數(shù)(取得身份證類型的 Id
)褒繁。
但這樣做的問題究竟是什么呢亦鳞?首先這樣的代碼是非常難以進行單元測試的,因為在測試的時候我們往往需要構造一些不同的測試場景(比如我們想傳入護照類型的 Id
)棒坏,但這種寫法導致你沒辦法改變其行為燕差。其次,我們在代碼的可維護性和擴展性方面有了很大的障礙坝冕,設想一下如果我們改變了 Address
的構造函數(shù)或 Id
的工廠方法的話徒探,我們不得不去更改 Person
類。一個類還好喂窟,但如果幾十個類都依賴 Address
或 Person
的話测暗,這會造成多大的麻煩吵血?
那么解決的方法呢?也很簡單偷溺,那就是我們把 Person
的構造改造一下:
class Person {
constructor(address, id) {
this.address = address;
this.id = id;
}
}
我們在構造中接受已經創(chuàng)建的 Address
和 Id
對象蹋辅,這樣在這段代碼中就沒有任何關于它們的具體實現(xiàn)了。換句話說挫掏,我們把創(chuàng)建這些依賴性的職責向上一級傳遞了出去(噗~~推卸責任罢炝怼)。現(xiàn)在我們在生產代碼中可以這樣構造 Person
:
const person = new Person(
new Address('北京', '北京', '朝陽區(qū)', 'xx街xx號'),
Id.getInstance(ID_TYPES.IDCARD)
);
而在測試時尉共,可以方便的構造各種場景褒傅,比如我們將地區(qū)改為遼寧:
const person = new Person(
new Address('遼寧', '沈陽', '和平區(qū)', 'xx街xx號'),
Id.getInstance(ID_TYPES.PASSPORT)
);
其實這就是依賴性注入了,這個概念是不是很簡單袄友?但有的同學問了殿托,那上一級要是單元測試不還是有問題嗎?是的剧蚣,如果上一級需要測試支竹,就得『推卸責任』到再上一級了。這樣一級一級的最后會推到最終的入口函數(shù)鸠按,但這也不是辦法啊礼搁,而且靠人工維護也很容易出錯,這時候就需要有一個依賴性注入的框架來解決了目尖,這種框架一般叫做 DI 框架或者 IoC 框架馒吴。這種框架對于熟悉 Java 和 .Net 的同學不會陌生,鼎鼎大名的 Spring 最初就是一個這樣的框架瑟曲,當然現(xiàn)在功能豐富多了饮戳,遠不止這個功能了。
Angular 中的依賴性注入框架
Angular 中的依賴性注入框架主要包含下面幾個角色:
- Injector(注入者):使用 Injector 提供的 API 創(chuàng)建依賴的實例
- Provider(提供者):Provider 告訴 Injector 怎樣 創(chuàng)建實例(比如我們上面提到的是通過某個構造函數(shù)還是工廠類創(chuàng)建等等)洞拨。Provider 接受一個令牌扯罐,然后把令牌映射到一個用于構建目標對象的工廠函數(shù)。
- Dependency(依賴):依賴是一種 類型 扣甲,這個類型就是我們要創(chuàng)建的對象的類型篮赢。
可能看到這里還是有些云里霧里,沒關系琉挖,我們還是用例子來說明:
import { ReflectiveInjector } from '@angular/core';
const injector = RelfectiveInjector.resolveAndCreate([
// providers 數(shù)組定義了多個提供者启泣,provide 屬性定義令牌
// useXXX 定義怎樣創(chuàng)建的方法
{ provide: Person, useClass: Person },
{ provide: Address, useFactory: () => {
if(env.testing)
return new Address('遼寧', '沈陽', '和平區(qū)', 'xx街xx號');
return new Address('北京', '北京', '朝陽區(qū)', 'xx街xx號');
}
},
{ 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 {
// 通過 @Inject 修飾器告訴 DI 這個參數(shù)需要什么樣類型的對象
// 請在 injector 中幫我找到并注入到對應參數(shù)中
constructor(@Inject(Address) address, @Inject(Id) id) {
// 省略
}
}
// 通過 injector 得到對象
const person = injector.get(Person);
上述代碼中,Angular 提供了 RelfectiveInjector
來解析和創(chuàng)建依賴的對象示辈,你可以看到我們把這個應用中需要的 Person
寥茫、 Id
和 Address
都放在里面了。誰需要這些對象就可以向 injector 請求矾麻,比如: injector.get(Person)
纱耻,當然也可以 injector.get(Address)
等等芭梯。可以把它理解成一個依賴性的池子弄喘,想要什么就取就好了玖喘。
但是問題來了,首先 injector 怎么知道如何創(chuàng)建你需要的對象呢蘑志?這個是靠 Provider 定義的累奈,在剛剛的 RelfectiveInjector.resolveAndCreate()
中我們發(fā)現(xiàn)它是接受一個數(shù)組作為參數(shù),這個數(shù)組就是一個 Provider 的數(shù)組急但。Provider 最常見的屬性有兩個澎媒。第一個是 provide
,這個屬性其實定義的是令牌波桩,令牌的作用是讓框架知道你要找的依賴是哪個然后就可以在 useXXX
這個屬性定義的構建方式中將你需要的對象構建出來了戒努。
那么 constructor(@Inject(Address) address, @Inject(Id) id)
這句怎么理解呢?由于我們在 const person = injector.get(Person);
想取得 Person 镐躲,但 Person 又需要兩個依賴參數(shù): address 和 id 储玫。 @Inject(Address) address
是告訴框架我需要的是一個令牌為 Address 的對象,這樣框架就又到 injector 中尋找令牌為 Address 對應的工廠函數(shù)匀油,通過工廠函數(shù)構造好對象后又把對象賦值到 address 缘缚。
由于這里我們是用對象的類型來做令牌,上面的注入代碼也可以寫成下面的樣子敌蚜。利用 Typescript 的類型定義,框架看到有依賴的參數(shù)就會去 Injector 中尋找令牌為該類型的工廠函數(shù)窝爪。
class Person {
constructor(address: Address, id: Id) {
// 省略
}
}
而對于令牌為類型的并且是 useClass
的這種形式弛车,由于前后都一樣,對于這種 Provider 我們有一個語法糖:可以直接寫成 { Person }
蒲每,而不用完整的寫成 { provide: Person, useClass: Person }
這種形式纷跛。當然還要注意 Token 不一定非得是某個類的類型,也可以是字符串邀杏, Angular 中還有 InjectionToken
用于創(chuàng)建一個可以避免重名的 Token贫奠。
那么其實除了 useClass
和 useFactory
,我們還可以使用 useValue
來提供一些簡單數(shù)據(jù)結構望蜡,比如我們可能希望把系統(tǒng)的 API 基礎信息配置通過這種形式讓所有想調用 API 的類都可以注入唤崭。如下面的例子中,基礎配置就是一個簡單的對象脖律,里面有多個屬性谢肾,這種情況用 useValue
即可。
{
provide: 'BASE_CONFIG',
useValue: {
uri: 'https://dev.local/1.1',
apiSecret: 'blablabla',
apiKey: 'nahnahnah'
}
}
依賴性注入進階
可能你注意到小泉,上面提到的依賴性注入有一個特點芦疏,那就是需要注入的參數(shù)如果在 Injector 中找不到對應的依賴冕杠,那么就會發(fā)生異常了。但確實有些時候我們是需要這樣的特性:該依賴是可選的酸茴,如果有我們就這么做分预,如果沒有就那樣做。遇到這種情況怎么辦呢薪捍?
Angular 提供了一個非常貼心的 @Optional
修飾器笼痹,這個修飾器用來告訴框架后面的參數(shù)需要一個可選的依賴。
constructor(@Optional(ThirdPartyLibrary) lib) {
if (!lib) {
// 如果該依賴不存在的情況
}
}
需要注意的是飘诗,Angular 的 DI 框架創(chuàng)建的對象都是單件( Singleton )的与倡,那么如果我們需要每次都創(chuàng)建一個新對象怎么破呢?我們有兩個選擇昆稿,第一種:在 Provider 中返回工廠而不是對象纺座,像下面例子這樣:
{
provide: Address,
useFactory: () => {
// 注意:這里返回的是工廠,而不是對象
return () => {
if(env.testing)
return new Address('遼寧', '沈陽', '和平區(qū)', 'xx街xx號');
return new Address('北京', '北京', '朝陽區(qū)', 'xx街xx號');
}
}
}
第二種:我們創(chuàng)建一個 child injector
(子注入者): Injector.resolveAndCreateChild()
const injector = ReflectiveInjector.resolveAndCreate([Person]);
const childInjector = injector.resolveAndCreateChild([Person]);
// 此時父 Injector 和子 Injector 得到的 Person 對象是不同的
injector.get(Person) !== childInjector.get(Person);
而且子 Injector 還有一個特性:如果在 childInjector
中找不到令牌對應的工廠溉潭,它會去父 Injector 中尋找净响。換句話說,這父子關系(多重的)是構成了一棵依賴樹喳瓣,框架會從最下面的子 Injector 開始尋找馋贤,一直找到最上面的父 Injector∥飞拢看到這里相信你就知道為什么父組件聲明的 providers 對于子組件是可見的配乓,因為子組件中在自己 constructor 中如果發(fā)現(xiàn)有找不到的依賴就會到父組件中去找。
在實際的 Angular 應用中我們其實很少會直接顯式使用 Injector 去完成注入惠毁,而是在對應的模塊犹芹、組件等的元數(shù)據(jù)中提供 providers 即可,這是由于 Angular 框架幫我們完成了這部分代碼鞠绰,它們其實在元數(shù)據(jù)配置后由框架放入 Injector 中了腰埂。
有問題的童鞋可以加入我的小密圈討論: http://t.xiaomiquan.com/jayRnaQ (該鏈接7天內(5月14日前)有效)
我的 《Angular 從零到一》紙書出版了,歡迎大家圍觀蜈膨、訂購屿笼、提出寶貴意見。
下面是書籍的內容簡介:
本書系統(tǒng)介紹Angular的基礎知識與開發(fā)技巧翁巍,可幫助前端開發(fā)者快速入門驴一。共有9章,第1章介紹Angular的基本概念曙咽,第2~7章從零開始搭建一個待辦事項應用蛔趴,然后逐步增加功能,如增加登錄驗證、將應用模塊化孝情、多用戶版本的實現(xiàn)鱼蝉、使用第三方樣式庫、動態(tài)效果制作等箫荡。第8章介紹響應式編程的概念和Rx在Angular中的應用魁亦。第9章介紹在React中非常流行的Redux狀態(tài)管理機制,這種機制的引入可以讓代碼和邏輯隔離得更好羔挡,在團隊工作中強烈建議采用這種方案洁奈。本書不僅講解Angular的基本概念和最佳實踐,而且分享了作者解決問題的過程和邏輯绞灼,講解細膩利术,風趣幽默,適合有面向對象編程基礎的讀者閱讀低矮。
慕課網 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