基於 TypeScript 的 IoC 和 DI

前言

在使用Angular或者Nestjs時,你可能會遇到下面這種形式的代碼:javascript

import { Component } from '@angular/core';
import { OtherService } from './other.service.ts';

@Component({
    // 組件屬性
})
export class AppComponent {
  constructor(public otherService: OtherService) {
    // 爲何這裏的otherService會被自動傳入
  }
}
複製代碼

上述代碼中使用了Component的裝飾器,並在模塊的providers中注入了須要使用的服務。這個時候,在AppComponentotherService將會自動獲取到OtherService實例。html

你可能會比較好奇,Angular是如何實現這種神奇操做的呢?實現的過程簡而言之,就是Angular在底層使用了IoC設計模式,並利用TypeScript強大的裝飾器特性,完成了依賴注入。下面我會詳細介紹IoC與DI,以及簡單的DI實例。java

理解了IoC與DI的原理,有助於咱們更好的理解和使用AngularNestjsgit

什麼是 IoC?

IoC 英文全稱爲 Inversion of Control,即控制反轉。控制反轉是面向對象編程中的一種原則,用於下降代碼之間的耦合度。傳統應用程序都是在類的內部主動建立依賴對象,這樣將致使類與類之間耦合度很是高,而且不容易測試。有了 IoC 容器以後,能夠將建立和查找依賴對象的控制權交給了容器,這樣對象與對象之間就是鬆散耦合了,方便測試與功能複用,整個程序的架構體系也會變得很是靈活。github

正常方式的引用模塊是經過直接引用,就像下面這個例子同樣:面試

import { ModuleA } from './module-A';
import { ModuleB } from './module-B';

class ModuleC {
  constructor() {
    this.a = new ModuleA();
    this.b = new ModuleB();
  }
}
複製代碼

這麼作會形成ModuleC依賴於ModuleAModuleB,產生了模塊間的耦合。爲了解決模塊間的強耦合性,IoC的概念就產生了。typescript

咱們經過使用一個容器來管理咱們的模塊,這樣模塊之間的耦合性就下降了(下面這個例子只是模仿 IoC 的過程,Container 須要另外實現):express

// container.js
import { ModuleA } from './module-A';
import { ModuleB } from './module-B';

// Container是咱們假設的一個模塊容器
export const container = new Container();
container.bindModule(ModuleA);
container.bindModule(ModuleB);

// ModuleC.js
import { container } from './container';
class ModuleC {
  constructor() {
    this.a = container.getModule('ModuleA');
    this.b = container.getModule('ModuleB');
  }
}
複製代碼

爲了讓你們更清楚 IoC 的過程,我舉一個例子,方便你們理解。編程

當我要找工做的時候,我會去網上搜索想要的工做崗位,而後去投遞簡歷,這個過程叫作控制正轉,也就是說控制權在個人手上。而對於控制反轉,找工做的過程就變成了,我把簡歷上傳到拉鉤這樣的第三方平臺(容器),第三方平臺負責管理不少人的簡歷。此時HR(其餘模塊)若是想要招人,就會按照條件在第三方平臺查詢到我,而後再聯繫安排面試。json

什麼是 DI?

DI 英文全稱爲 Dependency Injection,即依賴注入。依賴注入是控制反轉最多見的一種應用方式,即經過控制反轉,在對象建立的時候,自動注入一些依賴對象。

如何使用 TypeScript 實現依賴注入?

NestjsAngular中,咱們須要經過裝飾器@Injectable()讓咱們依賴注入到類實例中。而理解他們如何實現依賴注入,咱們須要先對裝飾器有所瞭解。下面咱們簡單的介紹一下什麼是裝飾器。

裝飾器(Decorator)

TypeScript中的裝飾器是基於ECMAScript標準的,而裝飾器提案仍處於stage2,存在不少不穩定因素,並且API在將來可能會出現破壞性的更改,因此該特性在TS中還是一個實驗性特性,默認是不啓用的(後面將會介紹如何配置開啓)。

裝飾器定義

裝飾器是一種特殊類型的聲明,它可以被附加到類聲明,方法,訪問符(getter, setter),屬性或參數上。裝飾器採用@expression這種形式進行使用。

下面是使用裝飾器的一個簡單例子:

function demo(target) {
  // 在這裏裝飾target
}

@demo
class DemoClass {}
複製代碼

裝飾器工廠

若是咱們須要定製裝飾器,這個時候就須要一個工廠函數,返回一個裝飾器,使用過程以下所示:

function decoratorFactory(value: string) {
  return function(target) {
    target.value = value;
  };
}
複製代碼

裝飾器組合

若是須要同時使用多個裝飾器,可使用@f @g x這種語法。

類裝飾器

類裝飾器是聲明在類定義以前,能夠用來監視、修改或替換類定義。類裝飾器接收的參數就是類自己。

function addDemo(target) {
  // 此處的target就是DemoClass
  target.demo = 'demo';
}

@addDemo
class DemoClass {}
複製代碼

方法、屬性、訪問器的裝飾器

裝飾器運行時會被當作函數執行,方法和訪問器接收下面三個參數:

  1. 對於靜態屬性來講是類的構造函數(Constructor),對於實例屬性是類的原型對象(Prototype)。
  2. 屬性(方法、屬性、訪問器)的名字。
  3. 屬性的屬性描述符(詳情查看這個文檔)。

特別地,對於屬性裝飾器只接收 1 和 2 這兩個參數,沒有第3個參數的緣由是由於沒法在定義原型對象時,描述實例上的屬性。

經過下面這個例子,咱們能夠具體看一下這三個參數是什麼,方便你們理解:

function decorator(target: any, key: string, descriptor: PropertyDescriptor) {}

class Demo {
  // target -> Demo.prototype
  // key -> 'demo1'
  // descriptor -> undefined
  @decorator
  demo1: string;

  // target -> Demo
  // key -> 'demo2'
  // descriptor -> PropertyDescriptor類型
  @decorator
  static demo2: string = 'demo2';

  // target -> Demo.prototype
  // key -> 'demo3'
  // descriptor -> PropertyDescriptor類型
  @decorator
  get demo3() {
    return 'demo3';
  }
  
  // target -> Demo.prototype
  // key -> 'method'
  // descriptor -> PropertyDescriptor類型
  method() {}
}
複製代碼

參數裝飾器

參數裝飾器聲明在一個參數聲明以前。運行時當作函數被調用,這個函數接收下面三個參數:

  1. 對於靜態屬性來講是類的構造函數,對於實例屬性是類的原型對象。
  2. 屬性(函數)的名字。
  3. 參數在函數參數列表中的索引。
function parameterDecorator( target: Object, key: string | symbol, index: number ) {}

class Demo {
  // target -> Demo.prototype
  // key -> 'demo1'
  // index -> 0
  demo1(@parameterDecorator param1: string) {
    return param1;
  }
}
複製代碼

TypeScript中的元數據(Metadata)

注意:元數據是 Angular 以及 Nestjs 依賴注入實現的基礎,請務必看完本章節。

由於Decorators是實驗性特性,因此若是想要支持裝飾器功能,須要在tsconfig.json中添加如下配置。

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  }
}
複製代碼

使用元數據須要安裝並引入reflect-metadata這個庫。這樣在編譯後的 js 文件中,就能夠經過元數據獲取類型信息。

// 引入reflect-metadata
import 'reflect-metadata';
複製代碼

大家應該會比較好奇,運行時JS是如何獲取類型信息的呢?請緊張地繼續往下看:

引入了reflect-metadata後,咱們就可使用其封裝在Reflect上的相關接口,具體請查看其文檔。而後在裝飾器函數中能夠經過下列三種metadataKey獲取類型信息。

  • design:type: 屬性類型
  • design:paramtypes: 參數類型
  • design:returntype: 返回值類型

具體能夠看下面的例子(每種類型的值都寫在註釋裏了):

const classDecorator = (target: Object) => {
  console.log(Reflect.getMetadata('design:paramtypes', target));
};

const propertyDecorator = (target: Object, key: string | symbol) => {
  console.log(Reflect.getMetadata('design:type', target, key));
  console.log(Reflect.getMetadata('design:paramtypes', target, key));
  console.log(Reflect.getMetadata('design:returntype', target, key));
};

// paramtypes -> [String] 即構造函數接收的參數
@classDecorator
class Demo {
  innerValue: string;
  constructor(val: string) {
    this.innerValue = val;
  }

  /* * 元數據的值以下: * type -> String * paramtypes -> undefined * returntype -> undefined */
  @propertyDecorator
  demo1: string = 'demo1';

  /* * 元數據的值以下: * type -> Function * paramtypes -> [String] * returntype -> String */
  @propertyDecorator
  demo2(str: string): string {
    return str;
  }
}
複製代碼

上面的代碼執行以後的返回以下所示:

[Function: Function] [ [Function: String] ] [Function: String]
[Function: String] undefined undefined
[ [Function: String] ]
複製代碼

我列出了各類裝飾器含有的元數據類型(即不是undefined的類型):

  • 類裝飾器: design:paramtypes
  • 屬性裝飾器: design:type
  • 參數裝飾器、方法裝飾器: design:typedesign:paramtypesdesign:returntype
  • 訪問器裝飾器: design:typedesign:paramtypes

依賴注入(DI)

說了那麼久,終於講到了本篇文檔最爲關鍵的內容了🎉,本節的實現請確保元數據在你的TS代碼中是可用的。

下面我給出一個簡單的實現依賴注入的 TS 實例:

// 構造函數類型
type Constructor<T = any> = new (...arg: any[]) => T;

// 類裝飾器,用於標識類是須要注入的
const Injectable = (): ClassDecorator => target => {};

// 須要注入的類
class InjectService {
  a = 'inject';
}

// 被注入的類
@Injectable()
class DemoService {
  constructor(public injectService: InjectService) {}

  test() {
    console.log(this.injectService.a);
  }
}

// 依賴注入函數Factory
const Factory = <T>(target: Constructor<T>): T => {
  // 獲取target類的構造函數參數providers
  const providers = Reflect.getMetadata('design:paramtypes', target);
  // 將參數依次實例化
  const args = providers.map((provider: Constructor) => new provider());
  // 將實例化的數組做爲target類的參數,並返回target的實例
  return new target(...args);
};

Factory(DemoService).test(); // inject
複製代碼

經過上述代碼中的Factory,咱們就成功地將InjectService注入到DemoService中。

咱們先看一下上面的代碼中DemoService編譯成 JS 以後的樣子:

// 此處省略了__decorate和__metadata的實現代碼
var DemoService = /** @class */ (function() {
  function DemoService(injectService) {
    this.injectService = injectService;
  }
  DemoService.prototype.test = function() {
    console.log(this.injectService.a);
  };
  DemoService = __decorate(
    [Injectable(), __metadata('design:paramtypes', [InjectService])],
    DemoService
  );
  return DemoService;
})();
複製代碼

從上面的代碼中,咱們看到 TS 將構造函數的參數類型[InjectService],經過元數據存儲了起來。因此在依賴注入的時候,咱們就能夠經過Reflect.getMetadata('design:paramtypes', target)取出了這個參數,並將其實例化後賦值到this.injectService中,這樣一個簡單的依賴注入就完成了。

若是你發現本文中有錯誤或者不合適的地方,歡迎留言反饋。

參考文獻

相關文章
相關標籤/搜索