基於TypeScript 的IoC 與DI

blank

基於TypeScript 的IoC 與DI

前言

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

import{Component}from'@angular/core';import{OtherService}from'./other.service.ts';@Component({// 组件属性
})exportclassAppComponent{constructor(publicotherService:OtherService){// 为什么这里的otherService会被自动传入
}}

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

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

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

什麼是IoC?

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

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

import{ModuleA}from'./module-A';import{ModuleB}from'./module-B';classModuleC{constructor(){this.a=newModuleA();this.b=newModuleB();}}

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

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

// container.jsimport{ModuleA}from'./module-A';import{ModuleB}from'./module-B';// Container是我們假設的一個模塊容器exportconstcontainer=newContainer();container.bindModule(ModuleA);container.bindModule(ModuleB);// ModuleC.jsimport{container}from'./container';classModuleC{constructor(){this.a=container.getModule('ModuleA');this.b=container.getModule('ModuleB');}}

為了讓大家更清楚IoC 的過程,我舉一個例子,方便大家理解。

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

什麼是DI?

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

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

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

裝飾器(Decorator)

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

裝飾器定義

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

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

function demo ( target ) { // 在这里装饰target } @ demo class DemoClass {}

裝飾器工廠

如果我們需要定制裝飾器,這個時候就需要一個工廠函數,返回一個裝飾器,使用過程如下所示:

functiondecoratorFactory(value:string){returnfunction(target){target.value=value;};}

裝飾器組合

如果需要同時使用多個裝飾器,可以使用@f @gx這種語法。

類裝飾器

類裝飾器是聲明在類定義之前,可以用來監視、修改或替換類定義。類裝飾器接收的參數就是類本身。

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

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

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

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

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

通過下面這個例子,我們可以具體看一下這三個參數是什麼,方便大家理解:

functiondecorator(target:any,key:string,descriptor:PropertyDescriptor){}classDemo{// target -> Demo.prototype// key -> 'demo1'// descriptor -> undefined@decoratordemo1:string;// target -> Demo// key -> 'demo2'// descriptor -> PropertyDescriptor類型@decoratorstaticdemo2:string='demo2';// target -> Demo.prototype// key -> 'demo3'// descriptor -> PropertyDescriptor類型@decoratorgetdemo3(){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 :返回值類型

具體可以看下面的例子(每種類型的值都寫在註釋裡了):

constclassDecorator=(target:Object)=>{console.log(Reflect.getMetadata('design:paramtypes',target));};constpropertyDecorator=(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]即構造函數接收的參數@classDecoratorclassDemo{innerValue:string;constructor(val:string){this.innerValue=val;}/**元數據的值如下:* type -> String* paramtypes -> undefined* returntype -> undefined*/@propertyDecoratordemo1:string='demo1';/**元數據的值如下:* type -> Function* paramtypes -> [String]* returntype -> String*/@propertyDecoratordemo2(str:string):string{returnstr;}}

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

[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 實例:

//構造函數類型typeConstructor<T=any>=new(...arg:any[])=>T;//類裝飾器,用於標識類是需要注入的constInjectable=():ClassDecorator=>target=>{};//需要注入的類classInjectService{a='inject';}//被注入的類@Injectable()classDemoService{constructor(publicinjectService:InjectService){}test(){console.log(this.injectService.a);}}//依賴注入函數FactoryconstFactory=<T>(target:Constructor<T>):T=>{//獲取target類的構造函數參數providersconstproviders=Reflect.getMetadata('design:paramtypes',target);//將參數依次實例化constargs=providers.map((provider:Constructor)=>newprovider());//將實例化的數組作為target類的參數,並返回target的實例returnnewtarget(...args);};Factory(DemoService).test();// inject

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

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

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

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

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

參考文獻

What do you think?

Written by marketer

blank

Vue組件庫工程探索與實踐之按需加載篇

blank

開發者必備— Web 無障礙手冊