在使用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
中注入了須要使用的服務。這個時候,在AppComponent
中otherService
將會自動獲取到OtherService
實例。html
你可能會比較好奇,Angular
是如何實現這種神奇操做的呢?實現的過程簡而言之,就是Angular
在底層使用了IoC設計模式,並利用TypeScript
強大的裝飾器特性,完成了依賴注入。下面我會詳細介紹IoC與DI,以及簡單的DI實例。java
理解了IoC與DI的原理,有助於咱們更好的理解和使用
Angular
及Nestjs
。git
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
依賴於ModuleA
和ModuleB
,產生了模塊間的耦合。爲了解決模塊間的強耦合性,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 英文全稱爲 Dependency Injection,即依賴注入。依賴注入是控制反轉最多見的一種應用方式,即經過控制反轉,在對象建立的時候,自動注入一些依賴對象。
在Nestjs
或Angular
中,咱們須要經過裝飾器@Injectable()
讓咱們依賴注入到類實例中。而理解他們如何實現依賴注入,咱們須要先對裝飾器有所瞭解。下面咱們簡單的介紹一下什麼是裝飾器。
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 和 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() {}
}
複製代碼
參數裝飾器聲明在一個參數聲明以前。運行時當作函數被調用,這個函數接收下面三個參數:
function parameterDecorator( target: Object, key: string | symbol, index: number ) {}
class Demo {
// target -> Demo.prototype
// key -> 'demo1'
// index -> 0
demo1(@parameterDecorator param1: string) {
return param1;
}
}
複製代碼
注意:元數據是 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:type
、design:paramtypes
、design:returntype
。design:type
、design:paramtypes
。說了那麼久,終於講到了本篇文檔最爲關鍵的內容了🎉,本節的實現請確保元數據在你的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
中,這樣一個簡單的依賴注入就完成了。
若是你發現本文中有錯誤或者不合適的地方,歡迎留言反饋。