前言

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

因为有使用过 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