TypeScript装饰器

前言

在做个人项目的REST API服务时,一个常见的问题就是API路由的各种Controllers对于权限的要求是不一致的,最显著的是有的访客即可访问,有的需要已登录状态的注册用户,有的可能还需要登录者拥有一定的权限,如果每个controller的方法里面都判断一次用户以及权限,这显然不是很优雅,造成了代码赘余。虽然Restify框架的APP实例可以使用各种中间件,例如cookie parser,body parser,但这种全局非条件式的并不适用于此种情况。

因为有使用过Python以及其flask框架的经验,最先想到的就是其装饰器。目前REST API使用的TypeScript,尝试着Google了一下ts decorator,果然,TypeScript中是有装饰器的,同样JavaScript ES7规范中装饰器也是一个重量级的特性。现在尝试着了解下TypeScript的装饰器。

TypeScript Decorators

首先,本文在查阅官方手册下所做的部分尝试。

首先装饰器是TypeScript的试验性质的功能,需要TS2.0并且需要开启特定的编译参数。

通过命令行

tsc --target ES5 --experimentalDecorators  

或者在tsconfig.json中指定

{
    "compilerOptions": {
        "target": "ES5",
        "experimentalDecorators": true
    }
}

在这之前,运行tsc -v确保打印主版本号2才可行。

TypeScript装饰器可应用于类、方法、参数、访问器以及属性,同时还可以作为反射元数据。

方法装饰器实例

REST API的Controller是一个类,而其中各种get,put,post方法需要不同的权限控制,因此适用方法装饰器。
这里建立一个Person类做示例:

function deco(target: any, propertyKey: string, descriptor: PropertyDescriptor) {  
    var originalMethod = descriptor.value
    descriptor.value = function (...args: any[]) {
        if (!this.name) {
            this.name = this.gender === 'male' ? `Mr ${args[0]}` : `Ms ${args[0]}`
        }
        console.log('Decorator call')
        originalMethod(...args)
    }
}

class Person {  
    public name: string
    public gender: string

    public constructor (gender: string) {
        this.gender = gender
    }

    @deco
    public setName(name: string): void {
        if (!this.name) {
            this.name = name
        }
        console.log('Original call')
    }
}

var person1 = new Person('male')  
person1.setName('Jack')

var person2 = new Person('female')  
person2.setName('Rose')

console.log(person1)  
console.log(person2)  

上述代码对Person类的实例方法setName使用了装饰器,目的是根据性别,改写person的称呼以取代默认的直接名字。

装饰器方法接收三个参数,分别为target, propertyKey以及descriptor

target在示例中为Person.prototype

propertyKey在示例中为setName,即被装饰的方法名字符串

descriptor是一个PropertyDescriptor,它定义了一个属性的access,这里不理解的推荐看MDN文档-Object.defineProperty

descriptor可以通过Object.getOwnPropertyDescriptor(target, propertyKey)获取。

tsc编译上述文件为ES5的js并使用node执行,可获得如下结果:

Decorator call  
Original call  
Decorator call  
Original call  
Person { gender: 'male', name: 'Mr Jack' }  
Person { gender: 'female', name: 'Ms Rose' }  

可以看见装饰器方法可以替代原有的SetName实例方法,而原有的方法执行与否是取决于装饰器方法的定义,本例中装饰器方法体内,仍然调用的被取代的原有方法,所以看以看到原方法的输出。

装饰器ES5 JS中的实现可以看tsc编译的js文件内容:

var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {  
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
function deco(target, propertyKey, descriptor) {  
    var originalMethod = descriptor.value;
    descriptor.value = function () {
        var args = [];
        for (var _i = 0; _i < arguments.length; _i++) {
            args[_i] = arguments[_i];
        }
        if (!this.name) {
            this.name = this.gender === 'male' ? "Mr " + args[0] : "Ms " + args[0];
        }
        console.log('Decorator call');
        originalMethod.apply(void 0, args);
    };
}
var Person = (function () {  
    function Person(gender) {
        this.gender = gender;
    }
    Person.prototype.setName = function (name) {
        if (!this.name) {
            this.name = name;
        }
        console.log('Original call');
    };
    return Person;
}());
__decorate([  
    deco
], Person.prototype, "setName", null);
var person1 = new Person('male');  
person1.setName('Jack');  
var person2 = new Person('female');  
person2.setName('Rose');  
console.log(person1);  
console.log(person2);  

首先定义了一个名为__decorate的装饰器装饰方法,用于将各种装饰器应用至目标。装饰器方法以及原有方法并无变动。最后调用__decorate方法应用装饰器。__decorate这个方法接收4个参数,后三个参数与装饰器方法保持一致。第一个参数则是一个装饰器方法数组,至于数组的排序,应该是与装饰器应用于方法上由内层向外层分别列出,因为靠内部的装饰器肯定是优先调用的。

更多参考

装饰器应用于其他如类、参数等的例子可以参考下面的Gist:

TypeScript Decorators