整理下TS装饰器(Decorator)的相关内容。

  • 装饰器种类
  • 装饰器工厂与组合使用
  • metadata reflecion API
  • 开源库中的使用

下面使用的TypeScript版本为3.6.3

为何用装饰器

  • 分离出通用的属性配置与函数功能
  • 增强代码可读性
  • 尝试新特性

装饰器种类

装饰器/属性 类装饰器 方法装饰器 访问器装饰器 属性装饰器 参数装饰器
位置 @foo class Bar {} @foo public bar() {} @foo get bar() @foo() bar: number bar(@foo para: string) {}
传入参数 constructor target, propertyKey, descriptor target, propertyKey, descriptor target, propertyKey target, propertyKey, parameterIndex
返回值 用返回值提供的构造函数来替换类的声明 返回值被用作方法的属性描述符 返回值被用作方法的属性描述符 被忽略 被忽略

参数释义:

  • constructor: 类构造函数
  • target: 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  • propertyKey: 成员的名字
  • descriptor: 成员的属性描述符
  • parameterIndex: 参数在函数参数列表中的索引

一个示例

类的声明与调用部分

@fooClassDcrt
class Student {
  constructor(name) {
    this.name = name;
  }

  @fooPropDcrt('female')
  sex: string;

  @fooAccessorDcrt(false)
  get name: string;

  @fooMethodDcrt('Good morning')
  say(@fooParaDcrt words) {
    console.log(words)
  }
}

const lihua = new Student('lihua')
lihua.sex = 'female'
lihua.say('hello decorator') // => Say hello decorator from fooMethodDcrt\n hello decorator
console.log(lihua.type) // => Student

装饰器部分

// 类装饰器
function fooClassDcrt<T extends {new(...args:any[]):{}}>(constructor:T) {
  return class extends constructor {
    type="Student"
  }
}
// 属性装饰器
function fooPropDcrt(value: string) {
  return (target: any, propertyKey: string | symbol) => {
    console.log(`Prop::${propertyKey} meta value: ${value}`)
  }
}
// 访问器装饰器
function fooAccessorDcrt(value: boolean) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        descriptor.configurable = value;
    };
}
// 方法装饰器
function fooMethodDcrt(value: string) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const method = descriptor.value
    descriptor.value = function (...args) {
      console.log(`Say ${value} from fooMethodDcrt`)
      method.apply(this, [...args])
    }
    return descriptor
  }
}
// 参数装饰器
function fooParaDcrt() {
  return function (target: any, propertyKey: string, parameterIndex) {
    console.log(propertyKey, parameterIndex)
  }
}

装饰器工厂

TS官方提案的描述:A decorator factory is a function that can accept any number of arguments, and must return one of the types of decorator.

传入任意数量的参数并返回一种装饰器,即一种装饰器函数构造器。可以在返回执行的装饰器方法前对传入数据进行一定处理并在装饰器中使用。

返回不同类型的装饰器

假如有一类具有同样记录数据的功能但不同类型的装饰器们:

@logClass
class foo {
  @logProp public bar: string;
  @logMethod handler(@logParam num: number) {}
}

虽然上述方式可以正常工作,但如果统一他们的调用会更加易用,像下面这样:

@log
class foo {
  @log public bar: string;
  @log handler(@log num: number) {}
}

这可以通过用一个装饰器工厂包裹不同装饰器函数来实现:

function log(...args : any[]) {
  switch(args.length) {
    case 1:
      return logClass.apply(this, args);
    case 2:
      return logProperty.apply(this, args);
    case 3:
      if(typeof args[2] === "number") {
        return logParam.apply(this, args);
      }
      return logMethod.apply(this, args);
    default:
      throw new Error("Decorators are not valid here!");
  }
}

传入自定义参数

一个普通的方法装饰器

class foo {
  @injectMethod
  bar() {};
}
function injectMethod(target, propertyKey, descriptor) {
  // 其他描述符与属性处理
  return descriptor
}

一个传参的方法装饰器

class foo {
  @logMethod('Hello world!')
  bar() {};
}
function logMethod (message: string) {
  return function(target, propertyKey, descriptor) {
    const method = descriptor.value
    descriptor.value = function (...args) {
      // 在装饰器工厂返回的函数中使用所传参数
      console.log(`Print: ${message}`)
      // 其他描述符与属性处理
      method.apply(this, ...args)
    }
    return descriptor
  }
}

组合使用

在同一目标上使用多个装饰器时的处理顺序:

  • 参数计算:从上至下
  • 装饰器执行:从下至上

示例:

function foo() {
    console.log("foo(): evaluated");
    return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
        console.log("foo(): called");
    }
}
function bar() {
    console.log("bar(): evaluated");
    return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
        console.log("bar(): called");
    }
}
class C {
    @foo()
    @bar()
    method() {}
}
// => foo(): evaluated
// => bar(): evaluated
// => bar(): called
// => foo(): called

metadata reflection API

反射(Reflect)一般指那些用来检验(inspect)其他代码的代码,部分使用场景:依赖注入/运行时类型断言/测试。

当JS应用变复杂时,需要使用一些(IoC容器)或特性(运行时类型断言)来处理复杂度,而由于JS中不存在反射的特性很难做到这一点。

一个反射API应允许在运行时检验一个未知对象,获取其不限于名称、类型、接口、参数等信息。

在JS中可以使用Object.getOwnPropertyDescriptor()Object.keys()来获取实体的信息,但还是需要反射去实现更强大的开发工具。目前在TS中可以使用reflect-metadata来实现反射特性,同时这个库是作为metadata reflection API的polyfill,可参考相关提案

在实现装饰器时可以使用如下几种design keys来获取反射元信息:

  1. design:type - 类型元信息

    import 'reflec-metadata'
    const logType = (value: string) => {
      return function(target: any, propertyKey: string) => {
        const type = Reflect.getMetadata("design:type", target, propertyKey);
        console.log(`${value} - ${propertyKey} type: ${type.name}`);
      }
    }
    class Foo { 
      @logType('Prop decorator')
      public barProp: string;
    }
    // => Prop decorator - barProp type: String
    
  2. design:paramtypes - 参数类型元信息

    import 'reflec-metadata'
    const logParamTypes = (target: any, propertyKey: string, descriptor) => {
      const types = Reflect.getMetadata("design:paramtypes", target, propertyKey);
      const s = types.map(type => type.name).join(',')
      console.log(`${propertyKey} param types: ${s}`);
    }
    class Foo { 
      @logParamTypes
      barMethod(name: string, age: number) { }
    }
    // => barMethod param types: String,Number
    
  3. design:returntype - 返回值元信息

    import 'reflec-metadata'
    const logReturnTypes = (target: any, propertyKey: string, descriptor) => {
      const type = Reflect.getMetadata("design:returntype", target, propertyKey);
      console.log(`${propertyKey} return types: ${type.name}`);
    }
    class Foo { 
      @logReturnTypes
      barMethod(age: number) : number { return age }
    }
    // => barMethod return types: Number
    

除了通过内置的design key获取外,也可以自定义元信息,以验证方法参数的场景为例:

  1. 定义校验方法参数的方法装饰器

    const requiredMetadataKey = Symbol("required");
    const CheckParams = (target: any, propertyKey: string, descriptor) => {
      let method = descriptor.value;
      descriptor.value = function () {
        let requiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey);
        requiredParameters.map(index => {
          if(arguments[index] === undefined) throw new Error('Missing required parameter')
        })
        return method.apply(this, arguments);
      }
    }
    
  2. 定义设置元信息的参数装饰器

    const required = (target: any, propertyKey: string,  parameterIndex: number) => {
      let existingRequiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
      existingRequiredParameters.push(parameterIndex);
      Reflect.defineMetadata(requiredMetadataKey, existingRequiredParameters, target, propertyKey);
    }
    
  3. 验证是否提供指定参数

    class Foo{ 
        @CheckParams
        bar(@required name: string, @required age: number, sex: string) { }
    }
    (new Foo()).bar('hello')
    // => Error: Missing required parameter
    

开源库中的使用

vue: vue-class-component & vue-property-decorator

如果使用ts编写vue项目并使用类组件的话少不了vue-class-componentvue-property-decorator,其中提供了vue相关组件与属性的装饰器,来看看它们的使用与实现。

在class类型的Vue组件中使用装饰器:

import { Component, Vue, Prop } from 'vue-property-decorator'

@Component
export default class FooComponent extends Vue {
    @Prop(Number) fooProp: number
}

首先是底层的装饰器类型与装饰器工厂的辅助方法:

// vue-class-component/src/utils.ts
// 三种装饰器类型接口
export interface VueDecorator {
  // Class decorator
  (Ctor: typeof Vue): void
  // Property decorator
  (target: Vue, key: string): void
  // Parameter decorator
  (target: Vue, key: string, index: number): void
}
// 装饰器工厂的辅助方法
export function createDecorator (factory: (options: ComponentOptions<Vue>, key: string, index: number) => void): VueDecorator {
  return (target: Vue | typeof Vue, key?: any, index?: any) => {
    // 在类构造函数的属性中保存装饰器的用于数据同步等方法(factory)
    const Ctor = typeof target === 'function'
      ? target as DecoratedClass
      : target.constructor as DecoratedClass
    if (!Ctor.__decorators__) {
      Ctor.__decorators__ = []
    }
    if (typeof index !== 'number') {
      index = undefined
    }
    Ctor.__decorators__.push(options => factory(options, key, index))
  }
}

其次是Compnent类装饰器工厂方法:

export function componentFactory (
  Component: VueClass<Vue>,
  options: ComponentOptions<Vue> = {}
): VueClass<Vue> {
  // 1. 解析原型的属性及方法等,赋值到options上
  const proto = Component.prototype
  Object.getOwnPropertyNames(proto).forEach(function (key) {...}
  // 2. 添加data hook收集类属性数据
  ;(options.mixins || (options.mixins = [])).push({
    data (this: Vue) {
      return collectDataFromConstructor(this, Component)
    }
  })
  // 3. 装饰器属性处理
  const decorators = (Component as DecoratedClass).__decorators__
  if (decorators) {
    // 执行已注册的装饰器
    decorators.forEach(fn => fn(options))
    delete (Component as DecoratedClass).__decorators__
  }
  // 4. 结合父类与options生成构造函数并返回
  const superProto = Object.getPrototypeOf(Component.prototype)
  const Super = superProto instanceof Vue
    ? superProto.constructor as VueClass<Vue>
    : Vue
  const Extended = Super.extend(options)
  return Extended
}

Prop等属性装饰器工厂就比较简单了:

// vue-property-ecorator/src/vue-property-decorator.ts
export function Prop(options: PropOptions | Constructor[] | Constructor = {}) {
  return (target: Vue, key: string) => {
    // 利用Reflect处理错误的类型问题
    applyMetadata(options, target, key)
    // 与target同步options中的属性配置,并保存该操作(__decorators__)
    createDecorator((componentOptions, k) => {
      ;(componentOptions.props || ((componentOptions.props = {}) as any))[ k ] = options
    })(target, key)
  }
}

可以看出,Vue类类型组件相关的装饰器函数会将不同方法与属性配置同步到父类的构造器上,即在实例化前注入相关属性。

server: routing-controllers

routing-controllers的装饰器作用与vue的类似,但它使用了另一种实现方式。

import { Controller, Body, Post } from "routing-controllers";

@Controller()
export class UserController {
  @Post("/users")
  post(@Body() user: any) {
      return "Saving user...";
  }
}

控制器类装饰器:

export function Controller(baseRoute?: string): Function {
  return function (object: Function) {
    getMetadataArgsStorage().controllers.push({
        type: "default",
        target: object,
        route: baseRoute
    });
  };
}

可以看出它使用了一个getMetadataArgsStorage的辅助方法,该方法会返回一个MetadataArgsStorage实例,该实例中保存注册的所有类型的metadata,且该实例会绑定到全局对象上,可随时访问所有metadata。

export class MetadataArgsStorage {
  // 保存构建metadata实例所需的args对象数组
  controllers: ControllerMetadataArgs[] = [];
  middlewares: MiddlewareMetadataArgs[] = [];
  actions: ActionMetadataArgs[] = [];
  ...
  // 过滤已存在类的参数对象
  filterControllerMetadatasForClasses(classes: Function[]): ControllerMetadataArgs[] {
    return this.controllers.filter(ctrl => {
      return classes.filter(cls => ctrl.target === cls).length > 0;
    });
  }
  ...
}

其中metadata实例会用于在创建Server时内部RoutingControllers的组件注册。

可以看出,这些装饰器函数中主要执行了实例化前的参数准备,使用独立的对象保存实参,便于最终实例化时的参数解析、服务注册与对象生成。

不仅是controller的类装饰器会预存实参,HTTP的方法装饰器与参数装饰器同样,如下所示:

export function Post(route?: string|RegExp): Function {
  return function (object: Object, methodName: string) {
    getMetadataArgsStorage().actions.push({
        type: "post",
        target: object.constructor,
        method: methodName,
        route: route
    });
  };
}
export function Body(options?: BodyOptions): Function {
  return function (object: Object, methodName: string, index: number) {
    getMetadataArgsStorage().params.push({
      type: "body",
      object: object,
      method: methodName,
      index: index,
      parse: false,
      required: options ? options.required : undefined,
      classTransform: options ? options.transform : undefined,
      validate: options ? options.validate : undefined,
      explicitType: options ? options.type : undefined,
      extraOptions: options ? options.options : undefined
    });
  };
}

mobx(状态管理),typedi(IoC)等库中也大量使用了装饰器,用法也各有不同,感兴趣的了解下。

参考

  1. Decorators & metadata reflection in TypeScript: From Novice to Expert
  2. rbuckton/reflect-metadata
  3. TypeScript Decorators: Property Decorators
  4. Decorators · TypeScript Handbook(中文版)
  5. Modifiers · MobX