從C#到TypeScript - 裝飾器

從C#到TypeScript - 裝飾器

在C#裏面如果想要不直接修改類或方法,但給類或方法添加一些額外的信息或功能,可以想到用Attribute,這是一個十分方便的功能裝飾器。
用TypeScript同樣也可以利用裝飾器來給類、函數、屬性以及參數添加附加功能,裝飾器是ES7的一個提案,在TypeScript裏已經有實現可用,不過需要在tsconfig.json裏啓用experimentalDecorators

"compilerOptions": {
    ..., // other options
    "experimentalDecorators": true}

裝飾器介紹

TypeScript中裝飾器可以應用到類、方法、屬性及函數參數上,而且可以同時應用多個。
裝飾器的寫法是@name()()可以不要,也可以在裏面寫一些參數。

@Testable
@Log('controller')class Controller{

    @GET
    getContent(@QueryParam arg: string): string{        return '';
    }
}

裝飾器的實現

裝飾器根據實現可以分兩種:
一種是不帶括號,和屬性一樣,如@Testable

function Testable(target: Function) { // 類、方法、屬性、方法參數的參數各不相同
    //這裏可以記錄一些信息到target,或者針對target做一些處理,如seal}

另外一種是帶括號的,和函數一樣,如@Log('controller'),實現函數裏的參數就是括號裏的參數,而且需要返回一個function

function Log(name: string) { // name就是傳進來的參數'controller'
    return function(target: Function) { // 類、方法、屬性、方法參數的參數各不相同
        // 這裏可以根據name和target來做一些處理
    }
}

類裝飾器

上面的(target: Function)其實就是類的裝飾器參數,指向的是類的構造函數,如果想給類加一個簡單的seal功能,可以這樣做:

function sealed(target: Function) {    Object.seal(target);    Object.seal(target.prototype);
}

@sealedclass Test{
    
}

Test.prototype.test = ''; // 運行時出錯,不能添加

上面的sealed就是類的裝飾器,target指構造函數,類裝飾器就這麼一個參數。

方法裝飾器

方法裝飾器的使用方法和類裝飾器類似,只是參數不一樣,方法裝飾器有三個參數:

  1. 如果裝飾的是靜態方法,則是類的構造函數,如果是實例方法則是類的原型。

  2. 方法的名字。

  3. 方法的PropertyDescriptor
    PropertyDescriptor即屬性描述符,有
    configurable 是否可以配置,如動態添加刪除函數屬性之類
    writable 是否可寫,可以用來設置只讀屬性
    enumerable 是否可枚舉,即是否能在for...in中能枚舉到
    value 對象或屬性的值

有了這些參數就可以很好的給方法添加一些功能,比如下面實現類型WebApi裏的Get的路由:

const Router = Symbol(); // 唯一key,用來存裝飾器的信息function GET(path?: string) { // GET帶了個可選參數
    return (target: any, name: string) => setMethodDecorator(target, name, 'GET', path);
}//把method和path存起來,路由查找的時候就可以用了function setMethodDecorator(target: any, name: string, method: string, path?: string){
    target[Router] = target[Router] || {};
    target[Router][name] = target[Router][name] || {};
    target[Router][name].method = method;
    target[Router][name].path = path;
}// 通過PropertyDescriptor來設置enumerablefunction Enumerable(enumerable: boolean) { 
    return (target: any, name: string, descriptor: PropertyDescriptor) => {
        descriptor.enumerable = enumerable;
    };
}class Controller{

    @GET
    @Enumerable(true)
    getContent(arg: string): string{        return '';
    }
}

參數裝飾器

方法參數同樣可以有裝飾器,同樣有三個參數,前兩個參數和方法的一致,最後一個參數是所裝飾的參數的位置。
能過參數裝飾器可以給方法動態的檢查或設置參數值,下面是檢查參數是否爲空,爲空則拋出異常。

const CheckNullKey = Symbol();const Router = Symbol();// 把CheckNull裝飾的參數存起來function CheckNull(target: any, name: string, index: number) {
    target[Router] = target[Router] || {};
    target[Router][name] = target[Router][name] || {};
    target[Router][name].params = target[Router][name].params || [];
    target[Router][name].params[index] = CheckNullKey;
}// 找出CheckNull的參數,並檢查參數值,爲空則拋異常,否則繼續執行方法function Check(target: any, name: string, descriptor: PropertyDescriptor) {    let method = descriptor.value;
    descriptor.value = function () {        let params = target[Router][name].params;        if (params) {            for (let index = 0; index < params.length; index++) {                if (params[index] == CheckNullKey &&  // 找到CheckNull的參數並拋異常
                    (arguments[index] === undefined || arguments[index] === null)) {                    throw new Error("Missing required argument.");
                }
            }
        }        return method.apply(this, arguments);
    }
}class Controller{

    @Check
    getContent(@CheckNull id: string): string{        console.info(id);        return id;
    }
}new Controller().getContent(null); // error : Missing required argument.

屬性裝飾器

用法同上,參數只有兩個,和類裝飾器的前兩個一樣,常用來標識屬性的特性。

function Column(target: any, name: string) {    //把name存起來,這個column僅僅是標識出來對應數據庫中的列,常用在ORM框架中}class Table{

    @Column  
    name: string;
}

另外還有屬性訪問器的裝飾器,和方法基本一樣,同樣的三個參數,不過同個屬性的getset只能有一個有,而且必須是先聲明的那個。

class User {    private _name: string;

    @Enumerable(true)    get name(){        return this._name;
    }    set name(value: string) {        this._name = value;
    }
}

多個裝飾器的執行順序

一個聲明可以添加多個裝飾器,所以會有個執行先後順序。
首先從上到下執行裝飾器函數,然後再從下往上應用帶括號的裝飾器返回的函數。

function Test1(){    console.info('eval test1');    return function(target: any, name: string, descriptor: PropertyDescriptor){        console.info('apply test1');
    }
}function Test2(){    console.info('eval test2');    return function(target: any, name: string, descriptor: PropertyDescriptor){        console.info('apply test2');
    }
}class User1{

    @test1()
    @Test2()
    getName(){

    }
}

結果是:

eval test1eval test2
apply test2
apply test1

總之,裝飾器等於引入了天然的裝飾模式,給類,方法等添加額外功能。不過裝飾器目前還不算太穩定,但是由於確實方便,已經有成熟項目在使用了。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章