基於TypeScript 的IoC 與DI
前言
在使用Angular
或者Nestjs
時,你可能會遇到下面這種形式的代碼:
import{Component}from'@angular/core';import{OtherService}from'./other.service.ts';@Component({// 组件属性
})exportclassAppComponent{constructor(publicotherService:OtherService){// 为什么这里的otherService会被自动传入
}}
上述代碼中使用了Component
的裝飾器,並在模塊的providers
中註入了需要使用的服務。這個時候,在AppComponent
中otherService
將會自動獲取到OtherService
實例。
你可能會比較好奇, Angular
是如何實現這種神奇操作的呢?實現的過程簡而言之,就是Angular
在底層使用了IoC設計模式,並利用TypeScript
強大的裝飾器特性,完成了依賴注入。下面我會詳細介紹IoC與DI,以及簡單的DI實例。
理解了IoC與DI的原理,有助於我們更好的理解和使用
Angular
及Nestjs
。
什麼是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
依賴於ModuleA
和ModuleB
,產生了模塊間的耦合。為了解決模塊間的強耦合性, 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 實現依賴注入?
在Nestjs
或Angular
中,我們需要通過裝飾器@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 {}
方法、屬性、訪問器的裝飾器
裝飾器運行時會被當做函數執行,方法和訪問器接收下面三個參數:
- 對於靜態屬性來說是類的構造函數(Constructor),對於實例屬性是類的原型對象(Prototype)。
- 屬性(方法、屬性、訪問器)的名字。
- 屬性的屬性描述符(詳情查看這個文檔)。
特別地,對於屬性裝飾器只接收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(){}}
參數裝飾器
參數裝飾器聲明在一個參數聲明之前。運行時當做函數被調用,這個函數接收下面三個參數:
- 對於靜態屬性來說是類的構造函數,對於實例屬性是類的原型對象。
- 屬性(函數)的名字。
- 參數在函數參數列表中的索引。
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:type
、design:paramtypes
、design:returntype
。 - 訪問器裝飾器:
design:type
、design: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
中,這樣一個簡單的依賴注入就完成了。
如果你發現本文中有錯誤或者不合適的地方,歡迎留言反饋。