很慚愧在阿里實習將近三個月沒有一點文章產出,同期入職的 熾翎 和 炬透 都產出了很多優秀的文章,如不想痛失薪資普調和年終獎?試試自動化測試!(基礎篇),不由感慨優秀的人都是有共同點的:善於總結沉澱,並且文筆還好(這點太羨慕了)。入職即將滿三個月,也就是說我三個多月沒寫過文章了。文筆拙劣,還請見諒。html
本篇文章是 MidwayJS 的系列推廣文章第一篇,本來我打算直接一篇搞定,作個MidwayJS開發後臺應用的教程就行了。可是在提筆前詢問過一些同窗,發現即便是已經有工做經驗的前端同窗中也有一部分沒有了解過TS裝飾器相關的知識,對於IoC機制也知之甚少(雖然沒學過Java的我一樣只是只知其一;不知其二),所以這篇文章會首先講解IoC機制(依賴注入)與TS裝飾器相關的知識,力求內容不枯燥,並使各位成功的對MidwayJS來電~前端
MidwayJS目前已經升級到Midway-Serverless體系,這可能會給沒接觸過Serverless、只是想學習框架自己的你帶來一些困擾。你能夠先閱讀其框架自己文檔,來只體驗框架自己做爲後端應用的能力。git
你可能沒有聽過Egg
,但你必定聽過或者使用過Koa
/Express
,Egg
基於Koa
並在其能力上作了加強,奉行**【約定優於配置】**,同時它又能做爲一款定製能力強的基礎框架,來使得你能基於本身的技術架構封裝出一套適合本身業務場景的框架。MidwayJS
正是基於Egg
,但在Egg
的基礎上作了一些較大的變更:github
更好的TS支持,能夠說寫MidwayJS比較舒服的一個地方就是它的TypeScript支持了,好比會做爲服務的接口定義會單獨存放於interface
, 提供的能力強大的裝飾器,與TypeORM這種TS支持好的框架協做起來更是愉悅。typescript
IoC機制的路由,以咱們下篇文章將要實現的接口爲例:編程
@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> {
// ...
}
// ...
}
複製代碼
(Midway同時保留了Egg的路由能力,即src/app/router.ts
的路由配置方式)json
這裏是否會讓你想到NestJS
?的確在路由這裏兩者的思想基本是相同的,但Midway的IoC機制底層基於 Injection,一樣是Midway團隊的做品。而且,Midway的IoC機制也是Midway-Serverless
能力的重要支持(這個咱們下篇文章纔會講到)。後端
生態複用,Egg與Koa的中間件大部分能在Midway應用中完美兼容,少部分暫不支持的也由官方團隊在快速兼容。設計模式
穩定支持,MidwayJS至今仍在快速發展迭代,同時也在阿里內部做爲Serverless基建的重要成員而受到至關的重視,因此你不用擔憂它後續的維護狀況。babel
下面的部分裏,咱們會講解這些東西:
首先咱們須要知道,JS與TS中的裝飾器不是一回事,JS中的裝飾器目前依然停留在 stage 2 階段,而且目前版本的草案與TS中的實現差別至關之大(TS是基於初版,JS目前已經第三版了),因此兩者最終的裝飾器實現必然有很是大的差別。
其次,裝飾器不是TS所提供的特性(如類型、接口),而是TS實現的ECMAScript提案(就像類的私有成員同樣)。TS實際上只會對stage-3以上的語言提供支持,好比TS3.7.5引入了可選鏈(Optional chaining)與空值合併(Nullish-Coalescing)。而當TS引入裝飾器時(大約在15年左右),JS中的裝飾器依然處於 stage-1 階段。其緣由是TS與Angular團隊PY成功了,Ng團隊再也不維護 AtScript,而TS引入了註解語法(Annotation)及相關特性。
可是並不須要擔憂,即便裝飾器永遠到達不了stage-3/4階段,它也不會消失的。有至關多的框架都是裝飾器的重度用戶,如Angular
、Nest
、Midway
等。對於裝飾器的實現與編譯結果會始終保留,就像JSX
同樣。若是你對它的歷史與發展方向有興趣,能夠讀一讀 是否應該在production裏使用typescript的decorator?(賀師俊賀老的回答)
爲何咱們須要裝飾器?在後面的例子中咱們會體會到裝飾器的強大與魅力,基於裝飾器咱們可以快速優雅的複用邏輯,提供註釋通常的解釋說明效果,以及對業務代碼進行能力加強。同時咱們本文的重點:依賴注入也能夠經過裝飾器來很是簡潔的實現。如今咱們可能暫時體會不到 強大、簡潔 這些關鍵詞,不急,安心讀下去。我會嘗試經過這篇文章讓你對TS裝飾器總體創建起一個認知,並在平常開發裏也愛上使用裝飾器。
因爲我自己並沒學習過Java以及Spring IoC,所以個人理解可能存在一些誤差,還請在評論區指出錯誤之處~
裝飾器與註解實際上也有必定區別,因爲並無學過Java,這裏就不與Java中的註解進行比較了。而只是說我所認爲的兩者差別:
metadata
)的注入,本質上不能起到任何修改行爲的操做,須要scanner
去進行掃描得到元數據並基於其去執行操做,註解的元數據纔有實際意義。但實際上,TS中的裝飾器一般是同時包含了這兩種效能的,它可能消費元數據的同時也提供了元數據供別的裝飾器消費。
在開始前,你須要確保在
tsconfig.json
中設置了experimentalDecorators
與emitDecoratorMetadata
爲true。
首先要明確地是,TS中的裝飾器實現本質是一個語法糖,它的本質是一個函數,若是調用形式爲@deco()
,那麼這個函數應該再返回一個函數來實現調用。
其次,你應該明白ES6中class的實質,若是不明白,推薦閱讀個人這篇文章: 從Babel編譯結果看ES6的Class實質
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
複製代碼
咱們發現,在以單純裝飾器方式@addProp
調用時,無論用它來裝飾哪一個類,起到的做用都是相同的,由於其中要複用的邏輯是固定的。咱們試試以@addProp()
的方式來調用:
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
複製代碼
如今咱們想要添加的屬性值就能夠由咱們決定了, 實際上因爲咱們拿到了原型對象,還能夠進行花式操做,可以解鎖更多神祕姿式~
方法裝飾器的入參爲 類的原型對象 屬性名 以及屬性描述符(descriptor),其屬性描述符包含writable
enumerable
configurable
,咱們能夠在這裏去配置其相關信息。
注意,對於靜態成員來講,首個參數會是類的構造函數。而對於實例成員(好比下面的例子),則是類的原型對象
function addProps(): MethodDecorator {
return (target, propertyKey, descriptor) => {
console.log(target);
console.log(propertyKey);
console.log(JSON.stringify(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! 並無被修改
複製代碼
你是否以爲有點想起來Object.defineProperty()
? 的確方法裝飾器也是藉助它來修改類和方法的屬性的,你能夠去TypeScript Playground看看TS對上面代碼的編譯結果。
相似於方法裝飾器,但它的入參少了屬性描述符。緣由則是目前沒有方法在定義原型對象成員同時去描述一個實例的屬性(建立描述符)。
function addProps(): PropertyDecorator {
return (target, propertyKey) => {
console.log(target);
console.log(propertyKey);
};
}
class A {
@addProps()
originProps: any;
}
複製代碼
屬性與方法裝飾器有一個重要做用是注入與提取元數據,這點咱們在後面會體現到。
參數裝飾器的入參首要兩位與屬性裝飾器相同,第三個參數則是參數在當前函數參數中的索引。
function paramDeco(params?: any): ParameterDecorator {
return (target, propertyKey, index) => {
console.log(target);
console.log(propertyKey);
console.log(index);
target.constructor.prototype.fromParamDeco = '呀呼!';
};
}
class B {
someMethod(@paramDeco() param1: any, @paramDeco() param2: any) {
console.log(`${param1} ${param2}`);
}
}
new B().someMethod('啊哈', '林不渡!');
// @ts-ignore
console.log(B.prototype.fromParamDeco);
複製代碼
參數裝飾器與屬性裝飾器都有個特別之處,他們都不能獲取到描述符descriptor,所以也就不能去修改其參數/屬性的行爲。可是咱們能夠這麼作:給類原型添加某個屬性,攜帶上與參數/屬性/裝飾器相關的元數據,並由下一個執行的裝飾器來讀取。(裝飾器的執行順序請參見下一節)
固然像例子中這樣直接在原型上添加屬性的方式是十分不推薦的,後面咱們會使用ES7的Reflect Metadata
來進行元數據的讀/寫。
假設如今咱們同時須要四種裝飾器,你會怎麼作?定義四種裝飾器而後分別使用嗎?也行,但後續你看着這一堆裝飾器可能會感受有點頭疼...,所以咱們能夠考慮接入工廠模式,使用一個裝飾器工廠來爲咱們根據條件吐出不一樣的裝飾器。
首先咱們準備好各個裝飾器函數:
(不建議把功能也寫在裝飾器工廠中,會形成耦合)
// @ts-nocheck
function classDeco(): ClassDecorator {
return (target: Object) => {
console.log('Class Decorator Invoked');
console.log(target);
};
}
function propDeco(): PropertyDecorator {
return (target: Object, propertyKey: string) => {
console.log('Property Decorator Invoked');
console.log(propertyKey);
};
}
function methodDeco(): MethodDecorator {
return ( target: Object, propertyKey: string, descriptor: PropertyDescriptor ) => {
console.log('Method Decorator Invoked');
console.log(propertyKey);
};
}
function paramDeco(): ParameterDecorator {
return (target: Object, propertyKey: string, index: number) => {
console.log('Param Decorator Invoked');
console.log(propertyKey);
console.log(index);
};
}
複製代碼
接着,咱們實現一個工廠函數來根據不一樣條件返回不一樣的裝飾器:
enum DecoratorType {
CLASS = 'CLASS',
METHOD = 'METHOD',
PROPERTY = 'PROPERTY',
PARAM = 'PARAM',
}
type FactoryReturnType =
| ClassDecorator
| MethodDecorator
| PropertyDecorator
| ParameterDecorator;
function decoFactory(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: any;
@decoFactory(DecoratorType.METHOD)
method(@decoFactory(DecoratorType.PARAM) param: string) {}
}
new C().method();
複製代碼
(注意,這裏在TS類型定義上彷佛有些問題,因此須要帶上頂部的@ts-nocheck
,在後續解決了類型報錯後,我會及時更新的TAT)
裝飾器求值順序來自於TypeScript官方文檔一節中的裝飾器說明。
類中不一樣聲明上的裝飾器將按如下規定的順序應用:
注意這個順序,後面咱們可以實現元數據讀寫,也正是由於這個順序。
當存在多個裝飾器來裝飾同一個聲明時,則會有如下的順序:
(有點相似洋蔥模型)
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
是屬於ES7的一個提案,其主要做用是在聲明時去讀寫元數據。TS早在1.5+版本就已經支持反射元數據的使用,目前想要使用,咱們還須要安裝reflect-metadata
與在tsconfig.json
中啓用emitDecoratorMetadata
選項。
你能夠將元數據理解爲用於描述數據的數據,如某個對象的鍵、鍵值、類型等等就可稱之爲該對象的元數據。咱們先不用太在乎元數據定義的位置,先作一個簡單的闡述:
爲類或類屬性添加了元數據後,構造函數的原型(或是構造函數,根據靜態成員仍是實例成員決定)會具備[[Metadata]]
屬性,該屬性內部包含一個Map結構,鍵爲屬性鍵,值爲元數據鍵值對。
reflect-metadata
提供了對Reflect對象的擴展,在引入後,咱們能夠直接從Reflect
對象上獲取擴展方法。
文檔見 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內部的方法hello都注入了元數據,並經過getMetadata(metadataKey, target)
這個方式取出了存放的元數據。
Reflect-metadata支持命令式(
Reflect.defineMetadata
)與聲明式(上面的裝飾器方式)的元數據定義
咱們注意到,注入在類上的元數據在取出時target爲這個類D,而注入在方法上的元數據在取出時target則爲實例d。緣由其實咱們實際上在上面的裝飾器執行順序提到了,這是因爲注入在方法、屬性、參數上的元數據其實是被添加在了實例對應的位置上,所以須要實例化才能取出。
Reflect容許程序去檢視自身,基於這個效果,咱們能夠在裝飾器運行時去檢查其類型相關信息,如目標類型、目標參數的類型以及方法返回值的類型,這須要藉助TS內置的元數據metadataKey來實現,以一個檢查入參的例子爲例:
訪問符裝飾器的屬性描述符會額外擁有
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<T> ) {
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);
};
}
複製代碼
這個例子來自於TypeScript官方文檔,但實際上不能正常執行。由於在通過裝飾器處理後,set方法的this將會丟失。但我猜測官方的用意只是展現
design:type
的用法。
在這個例子中,咱們基於Reflect.getMetadata('design:type', target, propertyKey);
獲取到了裝飾器對應聲明的屬性類型,並確保在setter
被調用時檢查值類型。
這裏的 design:type
便是TS的內置元數據,你能夠理解爲TS在編譯前還手動執行了@Reflect.metadata("design:type", Point)
。TS還內置了**design:paramtypes
(獲取目標參數類型)與design:returntype
(獲取方法返回值類型)**這兩種元數據字段來提供幫助。但有一點須要注意,即便對於基本類型,這些元數據也返回對應的包裝類型,如number
-> [Function: Number]
IoC的全稱爲 Inversion of Control,意爲控制反轉,它是OOP中的一種原則(雖然不在n大設計模式中,但實際上IoC也屬於一種設計模式),它能夠很好的解耦代碼。
在不使用IoC的狀況下,咱們很容易寫出來這樣的代碼:
import { A } from './modA';
import { B } from './modB';
class C {
constructor() {
this.a = new A();
this.b = new B();
}
}
複製代碼
乍一看可能沒什麼,但實際上類C會強依賴於A、B,形成模塊之間的耦合。要解決這個問題,咱們能夠這麼作:用一個第三方容器來負責管理容器,當咱們須要某個實例時,由這個容器來替咱們實例化並交給咱們實例。以Injcetion
爲例:
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');
}
}
複製代碼
如今A、B、C之間沒有了耦合,甚至當某個類D須要使用C的實例時,咱們也能夠把C交給IoC容器。
咱們如今可以知道IoC容器大概的做用了:容器內部維護着一個對象池,管理着各個對象實例,當用戶須要使用實例時,容器會自動將對象實例化交給用戶。
再舉個栗子,當咱們想要處對象時,會上Soul、Summer、陌陌...等等去一個個找,找哪一種的與怎麼找是由我本身決定的,這叫 控制正轉。如今我以爲有點麻煩,直接把本身的介紹上傳到世紀佳緣,若是有人看上我了,就會主動向我發起聊天,這叫 控制反轉。
DI的全稱爲Dependency Injection,即依賴注入。依賴注入是控制反轉最多見的一種應用方式,就如它的名字同樣,它的思路就是在對象建立時自動注入依賴對象。再以Injection
的使用爲例:
// provide意爲當前對象須要被綁定到容器中
// inject意爲去容器中取出對應的實例注入到當前屬性中
@provide()
export class UserService {
@inject()
userModel;
async getUser(userId) {
return await this.userModel.get(userId);
}
}
複製代碼
咱們不須要在構造函數中去手動this.userModel = xxx
了,容器會自動幫咱們作這一步。
咱們在最開始介紹了MidwayJS的路由機制,大概長這樣:
@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> {
// ...
}
}
複製代碼
(@provide()
來自於底層的IoC支持Injection
,Midway在應用啓動時會去掃描被@provide()
裝飾的對象,並裝載到容器中,這裏不是重點,能夠暫且跳過,咱們主要關注如何將裝飾器路由解析成路由表的形式)
咱們要解析的路由以下:
@controller('/user')
export class UserController {
@get('/all')
async getAllUser(): Promise<void> {
// ...
}
@post('/update')
async updateUser(): Promise<void> {
// ...
}
}
複製代碼
首先思考controller
和get
/post
裝飾器,咱們須要使用這幾個裝飾器注入哪些信息:
首先是對於整個類,咱們須要將path: "/user"
這個數據注入:
// 工具常量枚舉
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);
};
};
複製代碼
然後是方法裝飾器,咱們選擇一個高階函數(柯里化)去吐出各個方法的裝飾器,而不是爲每種方法定義一個。
// 方法裝飾器 保存方法與路徑
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!);
};
};
};
// 首先肯定方法,然後在使用時纔去肯定路徑
const get = methodDecoCreator(GET);
const post = methodDecoCreator(POST);
複製代碼
接下來咱們要作的事情就很簡單了:
const routeGenerator = (ins: Object) => {
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;
};
複製代碼
生成的結果大概是這樣:
[
{
path: '/user/all',
method: 'post',
methodName: 'getAllUser',
methodBody: [Function (anonymous)]
},
{
path: '/user/update',
method: 'get',
methodName: 'updateUser',
methodBody: [Function (anonymous)]
}
]
複製代碼
基於這種思路,咱們能夠很容易的寫一個使Koa支持IoC路由的工具。若是你有興趣,不妨擴展一下。好比說路由還有可能長這樣:
@controller('/user', { middleware:[mw1, mw2, ...] })
export class UserController {
@get('/all', { middleware:[mw11, mw22, ...] })
async getAllUser(): Promise<void> {
// ...
}
@get('/:uid')
async getUser(): Promise<void> {
// ...
}
@post('/update')
async updateUser(): Promise<void> {
// ...
}
}
複製代碼
新增了幾個地方:
要不要試試整活?
這個例子是否屬於IoC機制的體現可能會有爭議,但我我的認爲Reflect Metadata
的設計自己就是IoC的體現。若是你有別的見解,歡迎在評論區告知我。
我我的瞭解並使用過的TS依賴注入工具庫包括:
其中TypeDI
也是我平常使用較多的一個,若是你使用基本的Koa開發項目,不妨試一試TypeORM
+ TypeORM-TypeDI-Extensions
。咱們再看看上面呈現過的Injection
的例子:
@provide()
export class UserService {
@inject()
userModel;
async getUser(userId) {
return await this.userModel.get(userId);
}
}
複製代碼
實際上,一個依賴注入工具庫一定會提供的就是 從容器中獲取實例 與 注入對象到容器中的兩個方法,如上面的provide
與inject
,TypeDI的Service
與Inject
。
讀完這篇文章,我想你應該對TypeScript中的裝飾器與IoC機制有了大概的瞭解,若是你意猶未盡,不妨去看一下TypeScript對裝飾器、反射元數據的編譯結果,見TypeScript Playground。或者,若是你想早點開始瞭解MidwayJS,在閱讀文檔的基礎上,你也能夠瞅瞅我寫的這個簡單的Demo:Midway-Article-Demo,基於 Midway
+ TypeORM
+ SQLite3
,但請注意仍處於雛形,許多Midway的強大能力還沒有獲得體現,因此不要以這個Demo斷定Midway的能力,我會盡快完善這個Demo的。
下一篇,咱們會講解Midway的基礎能力,以及對Midway-Serverless:阿里巴巴淘系技術部面對Serverless交出的其中一份答卷的展望。本篇內容可能仍是有些枯燥,下一篇咱們就會進入歡樂的實戰環節啦~