細說 Angular 的依賴性注入

什麼是依賴性注入?

依賴性注入( Dependency Injection )其實不是 Angular 獨有的概念,這是一個已經存在很長時間的設計模式,也能夠叫作控制反轉 ( Inverse of Control )。咱們從下面這個簡單的代碼片斷入手來看看什麼是依賴性注入以及爲何要使用依賴性注入。javascript

class Person {
  constructor() {
    this.address = new Address('北京', '北京', '朝陽區', 'xx街xx號');
    this.id = Id.getInstance(ID_TYPES.IDCARD);
  }
}複製代碼

上面的代碼中,咱們在 Person 這個類的構造函數中初始化了咱們構建 Person 所須要的依賴類: AddressId ,其中 Address 是我的的地址對象,而 Id 是我的身份對象。這段代碼的問題在於除了引入了內部所需的依賴以外, 它知道了這些依賴建立的細節 ,好比它知道 Address 的構造函數須要的參數(省、市、區和街道地址)和這些參數的順序,它還知道 Id 的工廠方法和其參數(取得身份證類型的 Id )。html

但這樣作的問題到底是什麼呢?首先這樣的代碼是很是難以進行單元測試的,由於在測試的時候咱們每每須要構造一些不一樣的測試場景(好比咱們想傳入護照類型的 Id ),但這種寫法致使你沒辦法改變其行爲。其次,咱們在代碼的可維護性和擴展性方面有了很大的障礙,設想一下若是咱們改變了 Address 的構造函數或 Id 的工廠方法的話,咱們不得不去更改 Person 類。一個類還好,但若是幾十個類都依賴 AddressPerson 的話,這會形成多大的麻煩?前端

那麼解決的方法呢?也很簡單,那就是咱們把 Person 的構造改造一下:java

class Person {
  constructor(address, id) {
    this.address = address;
    this.id = id;
  }
}複製代碼

咱們在構造中接受已經建立的 AddressId 對象,這樣在這段代碼中就沒有任何關於它們的具體實現了。換句話說,咱們把建立這些依賴性的職責向上一級傳遞了出去(噗~~推卸責任啊)。如今咱們在生產代碼中能夠這樣構造 Person編程

const person = new Person(
  new Address('北京', '北京', '朝陽區', 'xx街xx號'),
  Id.getInstance(ID_TYPES.IDCARD)
);複製代碼

而在測試時,能夠方便的構造各類場景,好比咱們將地區改成遼寧:設計模式

const person = new Person(
  new Address('遼寧', '瀋陽', '和平區', 'xx街xx號'),
  Id.getInstance(ID_TYPES.PASSPORT)
);複製代碼

其實這就是依賴性注入了,這個概念是否是很簡單?但有的同窗問了,那上一級要是單元測試不仍是有問題嗎?是的,若是上一級須要測試,就得『推卸責任』到再上一級了。這樣一級一級的最後會推到最終的入口函數,但這也不是辦法啊,並且靠人工維護也很容易出錯,這時候就須要有一個依賴性注入的框架來解決了,這種框架通常叫作 DI 框架或者 IoC 框架。這種框架對於熟悉 Java 和 .Net 的同窗不會陌生,鼎鼎大名的 Spring 最初就是一個這樣的框架,固然如今功能豐富多了,遠不止這個功能了。api

Angular 中的依賴性注入框架

Angular 中的依賴性注入框架主要包含下面幾個角色:數組

  • Injector(注入者):使用 Injector 提供的 API 建立依賴的實例
  • Provider(提供者):Provider 告訴 Injector 怎樣 建立實例(好比咱們上面提到的是經過某個構造函數仍是工廠類建立等等)。Provider 接受一個令牌,而後把令牌映射到一個用於構建目標對象的工廠函數。
  • Dependency(依賴):依賴是一種 類型 ,這個類型就是咱們要建立的對象的類型。

Angular 中的依賴性注入框架

可能看到這裏仍是有些雲裏霧裏,不要緊,咱們仍是用例子來講明:數據結構

import { ReflectiveInjector } from '@angular/core';
const injector = RelfectiveInjector.resolveAndCreate([
  // providers 數組定義了多個提供者,provide 屬性定義令牌
  // useXXX 定義怎樣建立的方法
  { provide: Person, useClass: Person },
  { provide: Address, useFactory: () => {
        if(env.testing)
            return new Address('遼寧', '瀋陽', '和平區', 'xx街xx號');
        return new Address('北京', '北京', '朝陽區', '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 這個參數須要什麼樣類型的對象
  // 請在 injector 中幫我找到並注入到對應參數中
  constructor(@Inject(Address) address, @Inject(Id) id) {
    // 省略
  }
}

// 經過 injector 獲得對象
const person = injector.get(Person);複製代碼

上述代碼中,Angular 提供了 RelfectiveInjector 來解析和建立依賴的對象,你能夠看到咱們把這個應用中須要的 PersonIdAddress 都放在裏面了。誰須要這些對象就能夠向 injector 請求,好比: injector.get(Person) ,固然也能夠 injector.get(Address) 等等。能夠把它理解成一個依賴性的池子,想要什麼就取就行了。app

可是問題來了,首先 injector 怎麼知道如何建立你須要的對象呢?這個是靠 Provider 定義的,在剛剛的 RelfectiveInjector.resolveAndCreate() 中咱們發現它是接受一個數組做爲參數,這個數組就是一個 Provider 的數組。Provider 最多見的屬性有兩個。第一個是 provide ,這個屬性其實定義的是令牌,令牌的做用是讓框架知道你要找的依賴是哪一個而後就能夠在 useXXX 這個屬性定義的構建方式中將你須要的對象構建出來了。

那麼 constructor(@Inject(Address) address, @Inject(Id) id) 這句怎麼理解呢?因爲咱們在 const person = injector.get(Person); 想取得 Person ,但 Person 又須要兩個依賴參數: address 和 id 。 @Inject(Address) address 是告訴框架我須要的是一個令牌爲 Address 的對象,這樣框架就又到 injector 中尋找令牌爲 Address 對應的工廠函數,經過工廠函數構造好對象後又把對象賦值到 address 。

因爲這裏咱們是用對象的類型來作令牌,上面的注入代碼也能夠寫成下面的樣子。利用 Typescript 的類型定義,框架看到有依賴的參數就會去 Injector 中尋找令牌爲該類型的工廠函數。

class Person {
  constructor(address: Address, id: Id) {
    // 省略
  }
}複製代碼

而對於令牌爲類型的而且是 useClass 的這種形式,因爲先後都同樣,對於這種 Provider 咱們有一個語法糖:能夠直接寫成 { Person } ,而不用完整的寫成 { provide: Person, useClass: Person } 這種形式。固然還要注意 Token 不必定非得是某個類的類型,也能夠是字符串, Angular 中還有 InjectionToken 用於建立一個能夠避免重名的 Token。

那麼其實除了 useClassuseFactory ,咱們還可使用 useValue 來提供一些簡單數據結構,好比咱們可能但願把系統的 API 基礎信息配置經過這種形式讓全部想調用 API 的類均可以注入。以下面的例子中,基礎配置就是一個簡單的對象,裏面有多個屬性,這種狀況用 useValue 便可。

{
  provide: 'BASE_CONFIG',
  useValue: {
    uri: 'https://dev.local/1.1',
    apiSecret: 'blablabla',
    apiKey: 'nahnahnah'
  }
}複製代碼

依賴性注入進階

可能你注意到,上面提到的依賴性注入有一個特色,那就是須要注入的參數若是在 Injector 中找不到對應的依賴,那麼就會發生異常了。但確實有些時候咱們是須要這樣的特性:該依賴是可選的,若是有咱們就這麼作,若是沒有就那樣作。遇到這種狀況怎麼辦呢?

Angular 提供了一個很是貼心的 @Optional 修飾器,這個修飾器用來告訴框架後面的參數須要一個可選的依賴。

constructor(@Optional(ThirdPartyLibrary) lib) {
    if (!lib) {
    // 若是該依賴不存在的狀況
    }
}複製代碼

須要注意的是,Angular 的 DI 框架建立的對象都是單件( Singleton )的,那麼若是咱們須要每次都建立一個新對象怎麼破呢?咱們有兩個選擇,第一種:在 Provider 中返回工廠而不是對象,像下面例子這樣:

{ 
    provide: Address, 
    useFactory: () => {
        // 注意:這裏返回的是工廠,而不是對象
        return () => {
            if(env.testing)
                return new Address('遼寧', '瀋陽', '和平區', 'xx街xx號');
            return new Address('北京', '北京', '朝陽區', 'xx街xx號');
        }
    } 
  }複製代碼

第二種:咱們建立一個 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 中若是發現有找不到的依賴就會到父組件中去找。

在實際的 Angular 應用中咱們其實不多會直接顯式使用 Injector 去完成注入,而是在對應的模塊、組件等的元數據中提供 providers 便可,這是因爲 Angular 框架幫咱們完成了這部分代碼,它們其實在元數據配置後由框架放入 Injector 中了。

有問題的童鞋能夠加入個人小密圈討論:t.xiaomiquan.com/jayRnaQ (該連接7天內(5月14日前)有效)

個人 《Angular 從零到一》紙書出版了,歡迎你們圍觀、訂購、提出寶貴意見。

下面是書籍的內容簡介:

本書系統介紹Angular的基礎知識與開發技巧,可幫助前端開發者快速入門。共有9章,第1章介紹Angular的基本概念,第2~7章從零開始搭建一個待辦事項應用,而後逐步增長功能,如增長登陸驗證、將應用模塊化、多用戶版本的實現、使用第三方樣式庫、動態效果製做等。第8章介紹響應式編程的概念和Rx在Angular中的應用。第9章介紹在React中很是流行的Redux狀態管理機制,這種機制的引入可讓代碼和邏輯隔離得更好,在團隊工做中強烈建議採用這種方案。本書不只講解Angular的基本概念和最佳實踐,並且分享了做者解決問題的過程和邏輯,講解細膩,風趣幽默,適合有面向對象編程基礎的讀者閱讀。

京東連接:item.m.jd.com/product/120…

Angular從零到一
相關文章
相關標籤/搜索