一文速覽 TypeScript 裝飾器 與 IoC 機(jī)制
前言
本篇文章適用于對(duì) TypeScript 裝飾器缺少使用經(jīng)驗(yàn)或只是淺嘗輒止過的同學(xué),我將從 TypeScript 裝飾器的誕生背景開始,介紹不同種類裝飾器的使用場(chǎng)景和功能,再到 元數(shù)據(jù)反射 與 IoC 機(jī)制。相信讀完本文后,在以后使用 TypeScript 裝飾器時(shí),你會(huì)多一份踏實(shí):現(xiàn)在你清清楚楚得知道它們的運(yùn)作流程了!
TypeScript裝飾器簡(jiǎn)介
首先,裝飾器是什么?簡(jiǎn)單的說,裝飾器是一種應(yīng)用在類及其內(nèi)部成員的語法,它的本質(zhì)其實(shí)就是函數(shù)。我對(duì)這一語法抱有額外的熱情,則是因?yàn)樗芎芎玫仉[藏掉許多功能的實(shí)現(xiàn)細(xì)節(jié)。如:
@InternalChanges()
class?Foo?{?}
在 @InternalChanges() 這個(gè)裝飾器中,我們甚至能夠完全修改掉這個(gè)類的功能行為,而只需要這唯一的一行代碼。你可能會(huì)覺得這使得內(nèi)部實(shí)現(xiàn)過于黑盒,但仔細(xì)想想,可復(fù)用的裝飾器實(shí)際就相當(dāng)于 utils 方法,在提供給外部使用時(shí),我們本就不希望使用者需要關(guān)心內(nèi)部的邏輯。
而裝飾器的另外一個(gè)功能要使用的更加廣泛,也更加符合我上面所說的“我就希望它是黑盒的”,那就是元數(shù)據(jù)(元編程)相關(guān),這一點(diǎn)我們會(huì)在后面詳細(xì)展開。
其次我們需要知道,JavaScript 與 TypeScript 中的裝飾器完全不是一回事,JS中的裝飾器目前依然停留在 stage 2 階段,并且目前版本的草案與TS中的實(shí)現(xiàn)差異相當(dāng)之大(TS是基于第一版,JS目前已經(jīng)第三版了),所以二者最終的裝飾器實(shí)現(xiàn)必然有非常大的差異。
如果你曾使用過 TypeScript 裝飾器,不妨看看下面這張圖展示的當(dāng)前 JavaScript 裝飾器使用方式,就能感覺出現(xiàn)在二者的實(shí)現(xiàn)差異之大了:

嚴(yán)格的來說,裝飾器不是 TypeScript 所提供的特性(如類型別名與接口等),而是其實(shí)現(xiàn)的 ECMAScript提案(就像類的私有成員一樣)。
TS實(shí)際上只會(huì)對(duì) stage-3 以上的提案提供支持,比如 TS3.7版本 引入了可選鏈(Optional chaining)與空值合并(Nullish-Coalescing),我想這兩個(gè)語法目前應(yīng)該非常多的同學(xué)已經(jīng)重度使用了。而當(dāng) TypeScript 開始引入裝飾器支持 時(shí)(大約在15年左右,最先引入的 TypeScript 版本是 1.5 版本),ECMAScript 中的裝飾器依然處于 stage-1 階段。其原因是 TypeScript 與 Angular 團(tuán)隊(duì)達(dá)成了合作,Ng 團(tuán)隊(duì)不再維護(hù) AtScript,而 TypeScript 引入了注解語法(Annotation)及相關(guān)特性。
AtScript 最初構(gòu)建于 TypeScript 之上,但是又引入了一部分來自于 Dart 的語言特性。同時(shí) Angular 2.0 也是基于 AtScript 而開發(fā)的。同樣是在 TypeScript 1.5 版本,TypeScript 團(tuán)隊(duì)宣布許多 AtScript 的特性將被實(shí)現(xiàn)在 1.5 版本中,而 Angular 2.0 也將直接基于 TypeScript。
為什么叫 AtScript ?因?yàn)?Angular 中重度使用了裝飾器,
at即代表了@這一語法。
但是并不需要擔(dān)心,即使裝飾器永遠(yuǎn)到達(dá)不了stage-3/4 階段,它也不會(huì)消失的(更何況現(xiàn)在提案中的裝飾器和 TypeScript 裝飾器也不是一個(gè)東西了)。有相當(dāng)多的框架都是裝飾器的重度用戶,如Angular、Nest、Midway等。對(duì)于裝飾器的內(nèi)部實(shí)現(xiàn)與編譯結(jié)果會(huì)始終保留(但不能確定的是,在 JavaScript 裝飾器成功進(jìn)入最終階段后是否會(huì)發(fā)生變化),就像JSX一樣。
如果你對(duì)它的歷史與發(fā)展方向有興趣,可以讀一讀 是否應(yīng)該在production里使用typescript的decorator?(賀師俊賀老的回答)
為什么我們需要裝飾器?在后面的例子中我們會(huì)體會(huì)到裝飾器的強(qiáng)大與魅力,基于裝飾器我們能夠快速優(yōu)雅的復(fù)用邏輯,對(duì)業(yè)務(wù)代碼進(jìn)行能力增強(qiáng)。同時(shí)我們本文的重點(diǎn):依賴注入也將使用裝飾器的元數(shù)據(jù)反射能力來實(shí)現(xiàn)。
裝飾器與注解
由于我本身并沒學(xué)習(xí)過 Java 以及Spring IoC,因此我的理解可能存在一些偏差,還請(qǐng)?jiān)谠u(píng)論區(qū)指出錯(cuò)誤之處~
裝飾器與注解實(shí)際上也有一定區(qū)別,由于并沒有學(xué)過Java,這里就不與Java中的注解進(jìn)行比較了。而只是說我所認(rèn)為的二者差異:
注解 應(yīng)該如同字面意義一樣, 只是為某個(gè)被注解的對(duì)象提供元數(shù)據(jù)( metadata)的注入,本質(zhì)上不能起到任何修改行為的操作,需要額外的scanner去進(jìn)行掃描獲得元數(shù)據(jù)并基于其去執(zhí)行操作,注解的元數(shù)據(jù)才有實(shí)際意義。單純的裝飾器 沒法添加元數(shù)據(jù),只能基于已經(jīng)由注解注入的元數(shù)據(jù)來執(zhí)行操作,來對(duì)類以及內(nèi)部成員如方法、屬性、方法參數(shù)進(jìn)行某種特定的操作。
但實(shí)際上,TypeScript中的裝飾器通常是同時(shí)包含了這兩種效能的,它在消費(fèi)元數(shù)據(jù)的同時(shí),也能夠提供元數(shù)據(jù)供別的裝飾器消費(fèi)(通過裝飾器的先后執(zhí)行順序)。
不同類型的裝飾器及使用
如果要在本地運(yùn)行示例代碼,你需要確保在
tsconfig.json中啟用了experimentalDecorators與emitDecoratorMetadata。
類裝飾器
function?addProp(constructor:?Function)?{
??constructor.prototype.job?=?'fe';
}
@addProp
class?P?{
??job:?string;
??constructor(public?name:?string)?{}
}
let?p?=?new?P('林不渡');
console.log(p.job);?//?fe
復(fù)制代碼
我們發(fā)現(xiàn),在以單純裝飾器方式 @addProp 調(diào)用時(shí),不管用它來裝飾哪個(gè)類,起到的作用都是相同的,即修改類上的屬性。因?yàn)檫@里裝飾器的邏輯是固定的。這樣肯定不是我們?yōu)橄胍?,起碼得支持調(diào)用時(shí)傳入不同的參數(shù)來將屬性修改為不同的值吧?
試試以 @addProp() 的方式來調(diào)用:
function?addProp(param:?string):?ClassDecorator?{
??return?(constructor:?Function)?=>?{
????constructor.prototype.job?=?param;
??};
}
@addProp('fe+be')
class?P?{
??job:?string;
??constructor(public?name:?string)?{}
}
let?p?=?new?P('林不渡');
console.log(p.job);?//?fe+be
首先要明確地是,TS中的裝飾器實(shí)現(xiàn)本質(zhì)是一個(gè)語法糖,它的本質(zhì)是一個(gè)函數(shù),如果調(diào)用形式為@deco()(即上面的例子),那么這個(gè)函數(shù)應(yīng)該再返回一個(gè)函數(shù)來實(shí)現(xiàn)調(diào)用,所以 addProp 方法再次返回了一個(gè) ClassDecorator。應(yīng)用在不同位置的裝飾器將接受的參數(shù)是不同的,如這里的類裝飾器接受的參數(shù)將是類的構(gòu)造函數(shù)。
其次,你應(yīng)該明白ES6中class的實(shí)質(zhì),如果現(xiàn)在暫時(shí)不明白,推薦先閱讀我的這篇一點(diǎn)都沒技術(shù)含量的技術(shù)文章: 從 Babel 編譯結(jié)果看 ES6 的 Class 實(shí)質(zhì)。
現(xiàn)在我們想要添加的屬性值就可以由我們決定了, 實(shí)際上由于我們拿到了原型對(duì)象,還可以進(jìn)行更多操作,解鎖更多神秘姿勢(shì)。
方法裝飾器
方法裝飾器的入?yún)?類的原型對(duì)象 ?屬性名 以及屬性描述符(descriptor),其屬性描述符包含writable enumerable configurable ,我們可以在這里去配置其相關(guān)信息,如禁止這個(gè)方法再次被修改。
注意,對(duì)于靜態(tài)成員來說,首個(gè)參數(shù)會(huì)是類的構(gòu)造函數(shù)。而對(duì)于實(shí)例成員(比如下面的例子),則是類的原型對(duì)象。
function?addProps():?MethodDecorator?{
??return?(target,?propertyKey,?descriptor)?=>?{
????descriptor.writable?=?false;
??};
}
class?A?{
??@addProps()
??originMethod()?{
????console.log("I'm?Original!");
??}
}
const?a?=?new?A();
a.originMethod?=?()?=>?{
??console.log("I'm?Changed!");
};
//?仍然是原來的方法
a.originMethod();?//?I'm?Original!?
你是否有點(diǎn)想起來Object.defineProperty()?的確方法裝飾器也是借助它來修改類和方法的屬性的,你可以在 TypeScript Playground 中看看 TypeScript 對(duì)上面代碼的編譯結(jié)果。
屬性裝飾器
類似于方法裝飾器,但它的入?yún)⑸倭藢傩悦枋龇?strong>原因則是目前沒有方法在定義原型對(duì)象成員的同時(shí),去描述一個(gè)實(shí)例的屬性(創(chuàng)建描述符)。
function?addProps():?PropertyDecorator?{
??return?(target,?propertyKey)?=>?{
????console.log(target);
????console.log(propertyKey);
??};
}
class?A?{
??@addProps()
??originProps:?unknown;
}
屬性與方法裝飾器有一個(gè)重要作用是注入與提取元數(shù)據(jù),這點(diǎn)我們?cè)诤竺鏁?huì)體現(xiàn)到。
參數(shù)裝飾器
參數(shù)裝飾器的入?yún)⑹滓獌晌慌c屬性裝飾器相同,第三個(gè)參數(shù)則是參數(shù)在當(dāng)前函數(shù)參數(shù)中的索引。
function?paramDeco(params?:?any):?ParameterDecorator?{
??return?(target,?propertyKey,?index)?=>?{
????target.constructor.prototype.fromParamDeco?=?'Foo';
??};
}
class?B?{
??someMethod(@paramDeco()?param1:?unknown,?@paramDeco()?param2:?unknown)?{
????console.log(`${param1}??${param2}`);
??}
}
//?"A?B"
new?B().someMethod('A',?'B');
//?Foo
//?@ts-ignore
console.log(B.prototype.fromParamDeco);
參數(shù)裝飾器與屬性裝飾器都有個(gè)特別之處,他們都不能獲取到描述符descriptor,因此也就不能去修改其參數(shù)/屬性的行為。但是我們可以這么做:給類原型添加某個(gè)屬性,攜帶上與參數(shù)/屬性/裝飾器相關(guān)的元數(shù)據(jù),并由下一個(gè)執(zhí)行的裝飾器來讀取。(裝飾器的執(zhí)行順序請(qǐng)參見下一節(jié))。
當(dāng)然像例子中這樣直接在原型上添加屬性的方式是十分不推薦的,后面我們會(huì)使用 ES7 中的 Reflect Metadata 來進(jìn)行元數(shù)據(jù)的讀/寫。
裝飾器工廠
假設(shè)現(xiàn)在我們同時(shí)需要四種功能相近的裝飾器,你會(huì)怎么做?定義四種裝飾器然后分別使用嗎?也行,但后續(xù)你看著這一堆裝飾器可能會(huì)感覺有點(diǎn)頭疼...,因此我們可以考慮接入工廠模式,使用一個(gè)裝飾器工廠來為我們根據(jù)條件生成不同的裝飾器。
首先我們準(zhǔn)備好各個(gè)裝飾器函數(shù):
function?classDeco():?ClassDecorator?{
????return?(target:?Object)?=>?{
????????console.log('Class?Decorator?Invoked');
????????console.log(target);
????};
}
function?propDeco():?PropertyDecorator?{
????return?(target:?Object,?propertyKey:?string?|?symbol)?=>?{
????????console.log('Property?Decorator?Invoked');
????????console.log(propertyKey);
????};
}
function?methodDeco():?MethodDecorator?{
????return?(
????????target:?Object,
????????propertyKey:?string?|?symbol,
????????descriptor:?PropertyDescriptor
????)?=>?{
????????console.log('Method?Decorator?Invoked');
????????console.log(propertyKey);
????};
}
function?paramDeco():?ParameterDecorator?{
????return?(target:?Object,?propertyKey:?string?|?symbol,?index:?number)?=>?{
????????console.log('Param?Decorator?Invoked');
????????console.log(propertyKey);
????????console.log(index);
????};
}
接著,我們實(shí)現(xiàn)一個(gè)工廠函數(shù)來根據(jù)不同條件返回不同的裝飾器:
enum?DecoratorType?{
??CLASS?=?'CLASS',
??METHOD?=?'METHOD',
??PROPERTY?=?'PROPERTY',
??PARAM?=?'PARAM',
}
type?FactoryReturnType?=
??|?ClassDecorator
??|?MethodDecorator
??|?PropertyDecorator
??|?ParameterDecorator;
function?decoFactory(
??this:?any,
??type:?DecoratorType.CLASS,
??...args:?any[]
):?ClassDecorator;
function?decoFactory(
??this:?any,
??type:?DecoratorType.METHOD,
??...args:?any[]
):?MethodDecorator;
function?decoFactory(
??this:?any,
??type:?DecoratorType.PROPERTY,
??...args:?any[]
):?PropertyDecorator;
function?decoFactory(
??this:?any,
??type:?DecoratorType.PARAM,
??...args:?any[]
):?ParameterDecorator;
function?decoFactory(
??this:?any,
??type:?DecoratorType,
??...args:?any[]
):?FactoryReturnType?{
??switch?(type)?{
????case?DecoratorType.CLASS:
??????return?classDeco.apply(this,?args);
????case?DecoratorType.METHOD:
??????return?methodDeco.apply(this,?args);
????case?DecoratorType.PROPERTY:
??????return?propDeco.apply(this,?args);
????case?DecoratorType.PARAM:
??????return?paramDeco.apply(this,?args);
????default:
??????throw?new?Error('Invalid?DecoratorType');
??}
}
@decoFactory(DecoratorType.CLASS)
class?C?{
??@decoFactory(DecoratorType.PROPERTY)
??prop:?unknown;
??@decoFactory(DecoratorType.METHOD)
??method(@decoFactory(DecoratorType.PARAM)?param:?string)?{}
}
new?C().method('foobar');
以上是一種方式,你也可以通過判斷傳入的參數(shù),來判斷當(dāng)前的裝飾器被應(yīng)用在哪個(gè)位置。
多個(gè)裝飾器聲明的執(zhí)行順序
類中不同聲明上的裝飾器將按以下規(guī)定的順序應(yīng)用:
參數(shù)裝飾器,然后依次是方法裝飾器,訪問符裝飾器,或屬性裝飾器應(yīng)用到每個(gè)實(shí)例成員。 參數(shù)裝飾器,然后依次是方法裝飾器,訪問符裝飾器,或屬性裝飾器應(yīng)用到每個(gè)靜態(tài)成員。 參數(shù)裝飾器應(yīng)用到構(gòu)造函數(shù)。 類裝飾器應(yīng)用到類。
注意這個(gè)順序,后面我們能夠?qū)崿F(xiàn)元數(shù)據(jù)讀寫,也正是因?yàn)檫@個(gè)順序。
當(dāng)存在多個(gè)裝飾器來裝飾同一個(gè)聲明時(shí),則會(huì)有以下的順序:
首先,由上至下依次對(duì)裝飾器表達(dá)式求值,得到返回的真實(shí)函數(shù)(如果有的話)。 而后,求值的結(jié)果會(huì)由下至上依次調(diào)用。
這個(gè)執(zhí)行順序有點(diǎn)像洋蔥模型對(duì)吧?
function?foo()?{
????console.log("foo?in");
????return?function?(target,?propertyKey:?string,?descriptor:?PropertyDescriptor)?{
????????console.log("foo?out");
????}
}
function?bar()?{
????console.log("bar?in");
????return?function?(target,?propertyKey:?string,?descriptor:?PropertyDescriptor)?{
????????console.log("bar?out");
????}
}
class?A?{
????@foo()
????@bar()
????method()?{}
}
//?foo?in
//?bar?in
//?bar?out
//?foo?out
Reflect Metadata
基本元數(shù)據(jù)讀寫
Reflect Metadata是屬于 ES7 的一個(gè)提案,其主要作用是在聲明時(shí)去讀寫元數(shù)據(jù)。TS早在1.5+版本就已經(jīng)支持反射元數(shù)據(jù)的使用,目前想要使用,我們還需要安裝 reflect-metadata ,且在 tsconfig.json中啟用 emitDecoratorMetadata 選項(xiàng)。
你可以將元數(shù)據(jù)理解為用于描述數(shù)據(jù)的數(shù)據(jù),如某個(gè)對(duì)象的鍵、鍵值、類型等等就可稱之為該對(duì)象的元數(shù)據(jù)。做一個(gè)簡(jiǎn)單的闡述:
為類或類屬性添加了元數(shù)據(jù)后,構(gòu)造函數(shù)的原型(或是構(gòu)造函數(shù),根據(jù)靜態(tài)成員還是實(shí)例成員決定)會(huì)具有[[Metadata]]屬性,該屬性內(nèi)部包含一個(gè)Map結(jié)構(gòu),鍵為屬性鍵,值為元數(shù)據(jù)鍵值對(duì)。
reflect-metadata提供了對(duì) Reflect 對(duì)象的擴(kuò)展,在引入后,我們可以直接從 Reflect對(duì)象上獲取擴(kuò)展方法,并將其作為裝飾器使用:
文檔見 reflect-metadata,但不用急著看,其API命令還是很語義化的。
import?'reflect-metadata';
@Reflect.metadata('className',?'D')
class?D?{
??@Reflect.metadata('methodName',?'hello')
??public?hello():?string?{
????return?'hello?world';
??}
}
const?d?=?new?D();
console.log(Reflect.getMetadata('className',?D));
console.log(Reflect.getMetadata('methodName',?d));
可以看到,我們給類 D 與 D 內(nèi)部的方法hello都注入了元數(shù)據(jù),并通過getMetadata(metadataKey, target)這個(gè)方式取出了存放的元數(shù)據(jù)。
Reflect-metadata支持 命令式(
Reflect.defineMetadata) 與聲明式(上面的裝飾器方式)的元數(shù)據(jù)定義
我們注意到,注入在類上的元數(shù)據(jù)在取出時(shí) target 為這個(gè)類D,而注入在方法上的元數(shù)據(jù)在取出時(shí) target 則為實(shí)例d。原因其實(shí)我們實(shí)際上在上面的裝飾器執(zhí)行順序提到了,這是由于注入在方法、屬性、參數(shù)上的元數(shù)據(jù)實(shí)際上是被添加在了實(shí)例對(duì)應(yīng)的位置上,因此需要實(shí)例化才能取出。
內(nèi)置元數(shù)據(jù)
Reflect允許程序去檢視自身,基于這個(gè)效果,我們可以在裝飾器運(yùn)行時(shí)去檢查其類型相關(guān)信息,如目標(biāo)類型、目標(biāo)參數(shù)的類型以及方法返回值的類型,這需要借助 TypeScript 內(nèi)置的元數(shù)據(jù)metadataKey來實(shí)現(xiàn),以一個(gè)檢查入?yún)⒌睦訛槔?/p>
訪問符裝飾器的屬性描述符參數(shù)將會(huì)額外擁有
get與set方法,其他與屬性裝飾器相同
import?'reflect-metadata';
class?Point?{
??x:?number;
??y:?number;
}
class?Line?{
??private?_p0:?Point;
??private?_p1:?Point;
??@validate
??set?p0(value:?Point)?{
????this._p0?=?value;
??}
??get?p0()?{
????return?this._p0;
??}
??@validate
??set?p1(value:?Point)?{
????this._p1?=?value;
??}
??get?p1()?{
????return?this._p1;
??}
}
function?validate<T>(
??target:?any,
??propertyKey:?string,
??descriptor:?TypedPropertyDescriptor
)?{
??let?set?=?descriptor.set!;
??descriptor.set?=?function?(value:?T)?{
????let?type?=?Reflect.getMetadata('design:type',?target,?propertyKey);
????if?(!(value?instanceof?type))?{
??????throw?new?TypeError('Invalid?type.');
????}
????set(value);
??};
}
const?line?=?new?Line();
//?Error!
//?@ts-ignore
line.p0?=?{
??x:?1,
};
在這個(gè)例子中,我們基于 Reflect.getMetadata('design:type', target, propertyKey) 獲取到了裝飾器對(duì)應(yīng)聲明的屬性類型,并確保在 setter被調(diào)用時(shí)檢查值類型。
這里的 design:type 即是 TS 的內(nèi)置元數(shù)據(jù)key,也即是說 TS 在編譯前還手動(dòng)執(zhí)行了@Reflect.metadata("design:type", Point)。除了 design:key 以外,TS還內(nèi)置了**design:paramtypes(獲取目標(biāo)參數(shù)類型)與design:returntype(獲取方法返回值類型)**這兩種元數(shù)據(jù)字段來提供幫助。但有一點(diǎn)需要注意,即使對(duì)于基本類型,這些元數(shù)據(jù)也返回對(duì)應(yīng)的包裝類型,如number -> [Function: Number]
IoC
概念介紹:IoC、依賴注入、容器
IoC的全稱為 Inversion of Control,意為控制反轉(zhuǎn),它是OOP中的一種設(shè)計(jì)原則,常用于解耦代碼。
直接說概念多沒意思,讓我們來想象這樣一個(gè)場(chǎng)景:
有這么一個(gè)類 C,它的代碼內(nèi)部使用到了另外兩個(gè)類 A、B,需要去分別實(shí)例化它們。在不使用 IoC 的情況下,我們很容易寫出來這樣的代碼:
import?{?A?}?from?'./modA';
import?{?B?}?from?'./modB';
class?C?{
??constructor()?{
????this.a?=?new?A();
????this.b?=?new?B();
??}
}
乍一看可能沒什么,但實(shí)際上類 C 會(huì)強(qiáng)依賴于A、B,造成模塊之間的耦合。如果后續(xù) A、B 的實(shí)例化參數(shù)變化,或者是 A、B 內(nèi)部又依賴了別的類,那么維護(hù)起來簡(jiǎn)直是一團(tuán)亂麻。
要解決這個(gè)問題,我們可以這么做:
C 的內(nèi)部代碼只需要定義一個(gè) 類型為 A、B的成員。 用一個(gè)第三方容器來管理這些作為依賴的類 當(dāng)實(shí)例化 C 時(shí),由容器負(fù)責(zé)實(shí)例化 A、B,并注入到對(duì)應(yīng)的屬性上
以 Injection 為例:
import?{?Container?}?from?'injection';
import?{?A?}?from?'./A';
import?{?B?}?from?'./B';
const?container?=?new?Container();
container.bind(A);
container.bind(B);
class?C?{
??constructor()?{
????this.a?=?container.get('a');
????this.b?=?container.get('b');
??}
}
現(xiàn)在A、B、C之間沒有了耦合,甚至當(dāng)某個(gè)類 D 需要使用 C 的實(shí)例時(shí),我們也可以把 C 交給IoC容器,它會(huì)幫我們照看好的。
我們現(xiàn)在能夠知道 IoC 容器大概的作用了:容器內(nèi)部維護(hù)著一個(gè)對(duì)象池,管理著各個(gè)對(duì)象實(shí)例,當(dāng)用戶需要使用實(shí)例時(shí),容器會(huì)自動(dòng)將對(duì)象實(shí)例化交給用戶。
再舉個(gè)栗子,當(dāng)我們想要處對(duì)象時(shí),會(huì)上Soul、Summer、陌陌...等等去一個(gè)個(gè)找,找哪種的與怎么找是由我自己決定的,這叫 控制正轉(zhuǎn)?,F(xiàn)在我覺得有點(diǎn)麻煩,直接把自己的介紹上傳到世紀(jì)佳緣,如果有人對(duì)我感興趣了,就會(huì)主動(dòng)向我發(fā)起聊天,這叫 控制反轉(zhuǎn)。
DI的全稱為Dependency Injection,即依賴注入。依賴注入是控制反轉(zhuǎn)最常見的一種應(yīng)用方式,就如它的名字一樣,它的思路就是在對(duì)象創(chuàng)建時(shí)自動(dòng)注入依賴對(duì)象。再以 Injection 的使用為例,這次我們用上裝飾器:
//?provide意為當(dāng)前對(duì)象需要被綁定到容器中
//?inject意為去容器中取出對(duì)應(yīng)的實(shí)例注入到當(dāng)前屬性中
@provide()
export?class?UserService?{
?
??@inject()
??userModel;
??async?getUser(userId)?{
????return?await?this.userModel.get(userId);
??}
}
我們不需要在構(gòu)造函數(shù)中去手動(dòng) this.userModel = xxx 并且考慮需要的傳參了,容器會(huì)自動(dòng)幫我們做這一步。
基于 IoC 機(jī)制的路由簡(jiǎn)易實(shí)現(xiàn)
如果你用 NestJS、MidwayJS 寫過應(yīng)用,那么你肯定熟悉下面這樣的代碼:
@provide()
@controller('/user')
export?class?UserController?{
??@get('/all')
??async?getUser():?Promise<void>?{
????//?...
??}
??@get('/uid/:uid')
??async?findUserByUid():?Promise<void>?{
????//?...
??}
??@post('/uid/:uid')
??async?updateUser():?Promise<void>?{
????//?...
??}
}
這種基于裝飾器聲明路由的方式一直是我的心頭好,你可以通過裝飾器非常容易的定義路由層面的攔截器與中間件等操作,在 NestJS 中,還存在著 @Pipe @Guard @Catch @UseInterceptors 等非常多細(xì)粒度的裝飾器用于在控制器或者路由層面進(jìn)行操作。
可是你想過它們是如何實(shí)現(xiàn)的嗎?假設(shè)我們要解析的路由如下:
@controller('/user')
export?class?UserController?{
??@get('/all')
??async?getAllUser():?Promise<void>?{
????//?...
??}
??@post('/update')
??async?updateUser():?Promise<void>?{
????//?...
??}
}
首先思考 controller 和 get / post裝飾器,我們需要使用這幾個(gè)裝飾器注入哪些信息:
路徑 方法(方法裝飾器)
首先是對(duì)于整個(gè)類,我們需要將path: "/user"這個(gè)數(shù)據(jù)注入:
//?工具常量枚舉
export?enum?METADATA_MAP?{
??METHOD?=?'method',
??PATH?=?'path',
??GET?=?'get',
??POST?=?'post',
??MIDDLEWARE?=?'middleware',
}
const?{?METHOD,?PATH,?GET,?POST?}?=?METADATA_MAP;
export?const?controller?=?(path:?string):?ClassDecorator?=>?{
??return?(target)?=>?{
????Reflect.defineMetadata(PATH,?path,?target);
??};
};
而后是方法裝飾器,我們選擇一個(gè)高階函數(shù)去吐出各個(gè)方法的裝飾器,而不是為每種方法定義一個(gè)。
//?方法裝飾器?保存方法與路徑
export?const?methodDecoCreator?=?(method:?string)?=>?{
??return?(path:?string):?MethodDecorator?=>?{
????return?(_target,?_key,?descriptor)?=>?{
??????Reflect.defineMetadata(METHOD,?method,?descriptor.value!);
??????Reflect.defineMetadata(PATH,?path,?descriptor.value!);
????};
??};
};
//?首先確定方法,而后在使用時(shí)才去確定路徑
const?get?=?methodDecoCreator(GET);
const?post?=?methodDecoCreator(POST);
接下來我們要做的事情就很簡(jiǎn)單了:
拿到注入在類上元數(shù)據(jù)的根路徑 拿到每個(gè)方法上元數(shù)據(jù)的方法、路徑 拼接,生成路由表
const?routeGenerator?=?(ins:?Record<string,?unknown>)?=>?{
??const?prototype?=?Object.getPrototypeOf(ins);
??const?rootPath?=?Reflect.getMetadata(PATH,?prototype['constructor']);
??const?methods?=?Object.getOwnPropertyNames(prototype).filter(
????(item)?=>?item?!==?'constructor'
??);
??const?routeGroup?=?methods.map((methodName)?=>?{
????const?methodBody?=?prototype[methodName];
????const?path?=?Reflect.getMetadata(PATH,?methodBody);
????const?method?=?Reflect.getMetadata(METHOD,?methodBody);
????return?{
??????path:?`${rootPath}${path}`,
??????method,
??????methodName,
??????methodBody,
????};
??});
??console.log(routeGroup);
??return?routeGroup;
};
生成的結(jié)果大概是這樣:
[
??{
????path:?'/user/all',
????method:?'post',
????methodName:?'getAllUser',
????methodBody:?[Function?(anonymous)]
??},
??{
????path:?'/user/update',
????method:?'get',
????methodName:?'updateUser',
????methodBody:?[Function?(anonymous)]
??}
]
依賴注入工具庫(kù)
我個(gè)人了解并使用過的TS依賴注入工具庫(kù)包括:
TypeDI,TypeStack出品 TSYringe,微軟出品 Inversify,目前 JS/TS 中 star數(shù)最多的一個(gè) 依賴注入工具庫(kù) Injection,MidwayJS團(tuán)隊(duì)出品,是 MidwayJS 底層 IoC 的能力支持
我們?cè)倏纯瓷厦娉尸F(xiàn)過的Injection的例子:
@provide()
export?class?UserService?{
?
??@inject()
??userModel;
??async?getUser(userId)?{
????return?await?this.userModel.get(userId);
??}
}
實(shí)際上,一個(gè)依賴注入工具庫(kù)必定會(huì)提供的就是 從容器中獲取實(shí)例 與 注入對(duì)象到容器中的兩個(gè)方法,如上面的 provide與 inject,TypeDI的 Service 與 Inject,以及 Inversify 的 injectable 與 inject。
總結(jié)
讀完這篇文章,我想你應(yīng)該對(duì) TypeScript中 的裝飾器與 IoC 機(jī)制有了大概的了解,如果你意猶未盡,不妨去看一下 TypeScript 對(duì)裝飾器、反射元數(shù)據(jù)的編譯結(jié)果(原本想作為本文的收尾部分,但我個(gè)人覺得沒有特別具有技術(shù)含量的地方,所以還請(qǐng)自行根據(jù)需要擴(kuò)展~),如果不想自己本地再起一個(gè)項(xiàng)目,你也可以直接使用TypeScript Playground。
最后,強(qiáng)烈推薦嘗試一次全程重度使用裝飾器來開發(fā)項(xiàng)目,這里給一個(gè)可行的技術(shù)方案:
Midway Serverless,使用裝飾器聲明你的 Serverless 函數(shù),如果不想使用 Serverless ,你也可以使用 MidwayJS 來開發(fā) Node 服務(wù)。 TypeORM,使用裝飾器語法聲明你的數(shù)據(jù)庫(kù)表以及字段,結(jié)合 MidwayJS 官方提供的 ORM組件 來獲得絲滑體驗(yàn)。 TypeGraphQL,使用裝飾器語法聲明你的 GraphQL 對(duì)象類型,與 TypeORM 可以一體使用,這樣你就能夠同時(shí)修改數(shù)據(jù)庫(kù)表字段與 GraphQL Schema了。如果要與 Midway(Serverless)一同使用,需要 Apollo-Server-Midway。 Util-Decorators,提供基于裝飾器的公用方法,如節(jié)流防抖、錯(cuò)誤處理等。
