細說 Angular 的依賴性注入

什么是依賴性注入?

依賴性注入( 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 所需要的依賴類: AddressId 客冈,其中 Address 是個人的地址對象,而 Id 是個人身份對象稳强。這段代碼的問題在于除了引入了內部所需的依賴之外场仲, 它知道了這些依賴創(chuàng)建的細節(jié) ,比如它知道 Address 的構造函數(shù)需要的參數(shù)(省退疫、市渠缕、區(qū)和街道地址)和這些參數(shù)的順序,它還知道 Id 的工廠方法和其參數(shù)(取得身份證類型的 Id )褒繁。

但這樣做的問題究竟是什么呢亦鳞?首先這樣的代碼是非常難以進行單元測試的,因為在測試的時候我們往往需要構造一些不同的測試場景(比如我們想傳入護照類型的 Id )棒坏,但這種寫法導致你沒辦法改變其行為燕差。其次,我們在代碼的可維護性和擴展性方面有了很大的障礙坝冕,設想一下如果我們改變了 Address 的構造函數(shù)或 Id 的工廠方法的話徒探,我們不得不去更改 Person 類。一個類還好喂窟,但如果幾十個類都依賴 AddressPerson 的話测暗,這會造成多大的麻煩吵血?

那么解決的方法呢?也很簡單偷溺,那就是我們把 Person 的構造改造一下:

class Person {
  constructor(address, id) {
    this.address = address;
    this.id = id;
  }
}

我們在構造中接受已經創(chuàng)建的 AddressId 對象蹋辅,這樣在這段代碼中就沒有任何關于它們的具體實現(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)建的對象的類型篮赢。
Angular 中的依賴性注入框架
Angular 中的依賴性注入框架

可能看到這里還是有些云里霧里,沒關系琉挖,我們還是用例子來說明:

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 寥茫、 IdAddress 都放在里面了。誰需要這些對象就可以向 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贫奠。

那么其實除了 useClassuseFactory ,我們還可以使用 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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末印叁,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子军掂,更是在濱河造成了極大的恐慌轮蜕,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,743評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蝗锥,死亡現(xiàn)場離奇詭異跃洛,居然都是意外死亡,警方通過查閱死者的電腦和手機终议,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評論 3 385
  • 文/潘曉璐 我一進店門汇竭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人穴张,你說我怎么就攤上這事韩玩⊥祝” “怎么了围段?”我有些...
    開封第一講書人閱讀 157,285評論 0 348
  • 文/不壞的土叔 我叫張陵球及,是天一觀的道長。 經常有香客問我叮贩,道長,這世上最難降的妖魔是什么佛析? 我笑而不...
    開封第一講書人閱讀 56,485評論 1 283
  • 正文 為了忘掉前任益老,我火速辦了婚禮,結果婚禮上寸莫,老公的妹妹穿的比我還像新娘捺萌。我一直安慰自己,他們只是感情好膘茎,可當我...
    茶點故事閱讀 65,581評論 6 386
  • 文/花漫 我一把揭開白布桃纯。 她就那樣靜靜地躺著酷誓,像睡著了一般。 火紅的嫁衣襯著肌膚如雪态坦。 梳的紋絲不亂的頭發(fā)上盐数,一...
    開封第一講書人閱讀 49,821評論 1 290
  • 那天,我揣著相機與錄音伞梯,去河邊找鬼玫氢。 笑死,一個胖子當著我的面吹牛谜诫,可吹牛的內容都是我干的漾峡。 我是一名探鬼主播,決...
    沈念sama閱讀 38,960評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼喻旷,長吁一口氣:“原來是場噩夢啊……” “哼生逸!你這毒婦竟也來了?” 一聲冷哼從身側響起掰邢,我...
    開封第一講書人閱讀 37,719評論 0 266
  • 序言:老撾萬榮一對情侶失蹤牺陶,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后辣之,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體掰伸,經...
    沈念sama閱讀 44,186評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,516評論 2 327
  • 正文 我和宋清朗相戀三年怀估,在試婚紗的時候發(fā)現(xiàn)自己被綠了狮鸭。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,650評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡多搀,死狀恐怖歧蕉,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情康铭,我是刑警寧澤惯退,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站从藤,受9級特大地震影響催跪,放射性物質發(fā)生泄漏。R本人自食惡果不足惜夷野,卻給世界環(huán)境...
    茶點故事閱讀 39,936評論 3 313
  • 文/蒙蒙 一懊蒸、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧悯搔,春花似錦骑丸、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,757評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽铸豁。三九已至,卻和暖如春黄鳍,著一層夾襖步出監(jiān)牢的瞬間推姻,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,991評論 1 266
  • 我被黑心中介騙來泰國打工框沟, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留藏古,地道東北人。 一個月前我還...
    沈念sama閱讀 46,370評論 2 360
  • 正文 我出身青樓忍燥,卻偏偏與公主長得像拧晕,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子梅垄,可洞房花燭夜當晚...
    茶點故事閱讀 43,527評論 2 349

推薦閱讀更多精彩內容

  • 版本:Angular 5.0.0-alpha 依賴注入是重要的應用設計模式厂捞。它使用得非常廣泛,以至于幾乎每個人都稱...
    soojade閱讀 2,986評論 0 3
  • 一队丝、什么是依賴注入 控制反轉(IoC) 控制反轉的概念最早在2004年由Martin Fowler提出靡馁,是針對面向...
    Keriy閱讀 3,172評論 0 8
  • Spring Cloud為開發(fā)人員提供了快速構建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務發(fā)現(xiàn)机久,斷路器臭墨,智...
    卡卡羅2017閱讀 134,633評論 18 139
  • 1.傳遞幸福 DISC是個喪心病狂的社群胧弛。第一次聽見海峰老師說:加入雙證班成就個人,消滅李海峰侠畔。會場哄堂大笑结缚,我也...
    笨笨ym閱讀 1,919評論 1 4
  • 在學習Swift 3的過程中整理了一些筆記,如果想看其他相關文章可前往《Swift 3必看》系列目錄 swift ...
    沒故事的卓同學閱讀 16,440評論 14 85