細(xì)說(shuō) Angular 的依賴(lài)性注入

什么是依賴(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): AddressId ,其中 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) AddressPerson 的話(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)建的 AddressId 對(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)型。
Angular 中的依賴(lài)性注入框架
Angular 中的依賴(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)用中需要的 PersonIdAddress 都放在里面了。誰(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í)除了 useClassuseFactory ,我們還可以使用 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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 版本:Angular 5.0.0-alpha 依賴(lài)注入是重要的應(yīng)用設(shè)計(jì)模式。它使用得非常廣泛,以至于幾乎每個(gè)人都稱(chēng)...
    soojade閱讀 3,013評(píng)論 0 3
  • 一、什么是依賴(lài)注入 控制反轉(zhuǎn)(IoC) 控制反轉(zhuǎn)的概念最早在2004年由Martin Fowler提出,是針對(duì)面向...
    Keriy閱讀 3,205評(píng)論 0 8
  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,915評(píng)論 18 139
  • 1.傳遞幸福 DISC是個(gè)喪心病狂的社群。第一次聽(tīng)見(jiàn)海峰老師說(shuō):加入雙證班成就個(gè)人,消滅李海峰。會(huì)場(chǎng)哄堂大笑,我也...
    笨笨ym閱讀 1,974評(píng)論 1 4
  • 在學(xué)習(xí)Swift 3的過(guò)程中整理了一些筆記,如果想看其他相關(guān)文章可前往《Swift 3必看》系列目錄 swift ...
    沒(méi)故事的卓同學(xué)閱讀 16,484評(píng)論 14 85