likes
comments
collection
share

TS装饰器(Decorator)详解

作者站长头像
站长
· 阅读数 69

代码地址: github.com/slashspaces…

什么是装饰器

装饰器是一种设计模式的体现,可以想像成是一个wrapper,包裹在类声明方法属性或者参数 等目标上。它可以对包裹的目标做特定的处理。通过装饰器可以在不修改业务代码的情况下,在目标执行前后做特定操作,同时也可把重复的逻辑切分出去(AOP)。

装饰器一般用于处理一些与类以及类属性本身无关的逻辑。例如: 一个类方法的执行耗时统计或者记录日志,可以单独拿出来写成装饰器。

装饰器基本语法

💡 若要启用实验性的装饰器特性,必须tsconfig.json里启用experimentalDecorators编译器选项

创建装饰器的方式很简单,就是使用 @expression 这种形式,expression求值后必须为一个function (这个function会有一些预设的参数,这些参数被称作元信息 ),因此,装饰器又有两种不同的写法:

  1. 普通装饰器(无法传参) :expression本身就是一个function

    function color(target) {
        // do something with "target" ...
    }
    
    @color
    class Test{}
    
  2. 装饰器工厂(可以传参) : expression是一个函数,且返回一个function

    function color(value: string) { // 这是一个装饰器工厂
        return function (target) { //  这是装饰器
            // do something with "target" and "value"...
        }
    }
    
    @color('red')
    class Test{}
    

装饰器组合

多个装饰器可以同时装饰一个目标,就像下面的示例:

// 普通装饰器--无法传参
function Component1(target: Function) {
  target.prototype.id = 100
}
// 装饰器工厂--可以传参
function Component2(options: { id: number }) {
  // 返回一个装饰器
  return function (target: Function & typeof TestClass) {
    target.prototype.id = options.id
  }
 }

@Component1
@Component2({id: 200})
export class TestClass {
  static eleemntId: string;

  id!: number;

  printId(prefix: string = ''): string {
    return prefix + this.id;
  }
}

console.log(new TestClass().id) // 100

执行后结果为100,至于为什么是100而不是200,可以使用tsc将上面代码编译为js如下:

TS装饰器(Decorator)详解

因此我们能够得出结论:当多个装饰器应用于一个目标上,它们求值方式与复合函数相似

@f @g x
// or
@f
@g
class X {}

在这个模型下,当复合fg时,复合的结果(*f*∘ *g*)(*x*)等同于*f*(*g*(*x*))

同样的,在TypeScript里,当多个装饰器应用在一个声明上时会进行如下步骤的操作:

  1. 由上至下依次对装饰器表达式求值,求值的结果会被当作函数并依次放入decorators数组中
  2. 遍历decorators数组,倒序执行(从装饰器顺序上看是从下往上执行)

装饰器求值

类中不同声明上的装饰器将按以下规定的顺序应用:

  1. 实例属性

  2. 实例方法

    1. 有参数,则先执行参数的装饰器
  3. 静态属性

  4. 静态方法

    1. 有参数,则先执行参数的装饰器

如果同一个目标有多个类型相同的装饰器,总是先执行后面或下面的装饰器。

// 类装饰器
function logClass(id: number) {
  return function (target: any) {
    console.log('类装饰器', id)
  }
}

// 属性装饰器
function logAttribute(id: number) {
  return function (target: any, attrName: any) {
    if (id >= 100) {
      console.log('静态属性装饰器', id);
    } else {
      console.log('属性装饰器', id)
    }
  }
}

// 方法装饰器
function logMethod(id: number) {
  return function (target: any, methodName: any, desc: any) {
    if (id >= 100) {
      console.log('静态方法装饰器', id)
    } else {
      console.log('方法装饰器', id)
    }
  }
}

// 方法参数装饰器
function logParmas(id: number) {
  return function (target: any, methodName: any, paramsIndex: any) {
    if (id >= 100) {
      console.log('静态方法参数装饰器', id)
    } else {
      console.log('方法参数装饰器', id)
    }
  }
}

@logClass(1)
@logClass(2)
class HttpClient {

  @logAttribute(100)
  static id = Math.random()

  @logMethod(100)
  static setId(@logParmas(100.1) id: number) {
    HttpClient.id = id
  }

  @logAttribute(1)
  public url: string | undefined

  constructor() { }

  @logMethod(1)
  getData() { }

  @logMethod(2)
  setData(@logParmas(2.1) attr1: any, @logParmas(2.2) attr2: any) {}
}

const http = new HttpClient()
/**
属性装饰器 1

方法装饰器 1

方法参数装饰器 2.2
方法参数装饰器 2.1
方法装饰器 2

静态属性装饰器 100

静态方法参数装饰器 100.1
静态方法装饰器 100

类装饰器 2
类装饰器 1
*/

装饰器的种类

根据装饰的目标不同,装饰器也可以分为几种类型

本章将会介绍以下几种装饰器:

  1. 类装饰器
  2. 属性装饰器
  3. 方法装饰器
  4. 参数装饰器

TS装饰器(Decorator)详解

Class Decorator

类装饰器在类声明之前被声明(紧靠着类声明)。 类装饰器应用于类构造函数,可以用来监视,修改或替换类定义

declare type ClassDecorator = <TFunction extends Function>(
  target: TFunction // 目标类构造器
) => 
  TFunction // 如果返回一个类构造器,则用返回的类构造器替换被装饰的类
  | 
  void;
  1. 类装饰器表达式会在运行时当作函数被调用,类的构造函数作为其唯一的参数。

    /**
     * @param target 被装饰类的构造器
     */
    const Component: ClassDecorator = (target: Function) => {
      target.prototype.id = 100
    }
    
    @Component
    export class TestClass {
      id!: number;
    
      printId(prefix: string = ''): string {
        return prefix + this.id
      }
    }
    
    console.log(new TestClass().id); // 100
    

    如果装饰器需要参数,我们可以使用装饰器工厂(在装饰器函数中返回一个函数来定义的)

    /**
     * @param id 参数
     * @returns  {ClassDecorator}
     */
    const ComponentWithId = (id: number): ClassDecorator => {
      return (target: Function) => {
        target.prototype.id = id
      }
    }
    
    @ComponentWithId(200)
    export class TestClass {
      id!: number;
    
      printId(prefix: string = ''): string {
        return prefix + this.id
      }
    }
    
    console.log(new TestClass().id);
    

    具体内部实现可以查看tsc编译后的结果

    TS装饰器(Decorator)详解

    可以看到,其实内部是调用了Reflect.decorate() 方法,我们可以通过自己调用该方法来使用装饰器:

    import 'reflect-metadata'
    /**
     * @param id 参数
     * @returns  {ClassDecorator}
     */
    const ComponentWithId = (id: number): ClassDecorator => {
      return (target: Function) => {
        target.prototype.id = id
      }
    }
    
    class TestClass {
      id!: number;
    
      printId(prefix: string = ''): string {
        return prefix + this.id
      }
    }
    
    const TC = Reflect.decorate([ComponentWithId(200)], TestClass)
    console.log((new TC.prototype.constructor).id); // 200
    
  2. 如果返回一个类构造器,则用返回的类构造器替换被装饰的类

    type NewType = new (...args: any[]) => any;
    
    /**
     *
     * 如果类装饰器返回一个类构造器,则用返回的类构造器替换被装饰的类
     * @param target
     * @returns
     */
    const Component = <T extends NewType>(target: T) => {
      return class extends target {
        constructor(...args: any[]) {
          console.log('Component constructor');
          super()
          this.id = 10
        }
    
        printId() {
          return `this is Component id: ${this.id}`
        }
      }
    }
    
    @Component
    export class TestClass {
      id!: number
    
      printId(prefix: string = ''): string {
        return prefix + this.id
      }
    }
    
    console.log(new TestClass().printId())
    
    // Component constructor
    // this is Component id: 10
    

Method Decorator

方法装饰器声明在一个方法的声明之前(紧靠着方法声明)。 它会被应用到方法的属性描述符上,可以用来监视,修改或者替换方法定义

/**
 * @param target 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象
 * @param propertyKey 方法名
 * @param propertyDescriptor 方法的属性描述符
 */
 type MethodDecorator = <T>(
  target: Object,
  propertyKey: string | symbol,
  descriptor: TypedPropertyDescriptor<T>)
=> TypedPropertyDescriptor<T> | void;

/**
 * 属性描述符
 */
interface PropertyDescriptor {
  // 能否使用delete, 能否修改方法特性或修改访问器属性
  configurable?: boolean
  // 可遍历?
  enumerable?: boolean
  // 用于定义新的方法,替换就仿佛
  value?: any
  // 可写?
  writable?: boolean
  // get/set 访问器
  get?(): any
  set?(v: any): void
}

下面是一个案例:


const methodDecorator: MethodDecorator = (target: Object, propertyKey: string | symbol, propertyDescriptor: PropertyDescriptor) => {
  console.log(target, propertyKey, propertyDescriptor);
  propertyDescriptor.value = function (...args: any[]) {
    return `Hello ${args}`
  }
}

export class TestClass {
  static elementId: string;
  id: number = 0

  @methodDecorator
  printId(prefix: string = ''): string {
    return prefix + this.id
  }
}

console.log(new TestClass().printId('World'));

上述代码编译后结果如下如:

TS装饰器(Decorator)详解

Parameter Decorator

参数装饰器声明在一个参数声明之前(紧靠着参数声明)。 参数装饰器应用于类构造函数或方法声明。

const parameterDecorator: ParameterDecorator = (
  target: Object, // 对于静态方法参数来说是构造函数,对于实例方法来说是类的原型对象
  propertyKey: string | symbol, // 方法名
  parameterIndex: number // 参数数组中的下标
) => {
  console.log(target, propertyKey, parameterIndex);
}

export class TestClass {
  static elementId: string

  constructor(private id: number) {}

  static staticMethod(@parameterDecorator prefix: string) { }

  printId(@parameterDecorator prefix: string): string {
    return prefix + this.id
  }
}

console.log(TestClass.staticMethod(''))
console.log(new TestClass(1).printId(''))
// {} printId 0
// [class TestClass] staticMethod 0

Property Decorator

属性装饰器声明在一个属性声明之前(紧靠着属性声明)

declare type PropertyDecorator = (
  target: Object,  // 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  propertyKey: string | symbol) // 成员的名字
=> void;

下面是一个案例

const propertyDecorator: PropertyDecorator = (target: Object, propertyKey: string | symbol) => {
  let value: number
  Reflect.defineProperty(target, propertyKey, {
    get() {
      console.log('Getting value ...');
      return value
    },
    set(newVal: number) {
      value = newVal
    }
  })
}

export class TestClass {
  static elementId: string;

  @propertyDecorator
  id: number = 0

  printId(prefix: string = ''): string {
    return prefix + this.id
  }
}

console.log(new TestClass().id);

// Getting value ...
// 0

上述代码编译后结果如下图:

TS装饰器(Decorator)详解

元数据

又称中介数据中继数据,为描述数据的数据(data about data),主要是描述数据属性(property)的信息,用来支持如指示存储位置、历史数据、资源查找、文件记录等功能。

而装饰器中的元信息反射使用非常简单,外观上仅仅可以看做在类的某个方法上附加一些随时可以获取的信息而已。

关于元数据的详细解释,可以阅读阮一峰老师的一篇文章元数据(MetaData)

使用之前我们必须先安装reflect-metadata这个库

npm i reflect-metadata --save

并且在tsconfig.json中启用原信息配置

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

reflect-metadata语法

// 在对象或对象的属性上定义元数据
Reflect.defineMetadata(metadataKey, metadataValue, target);
Reflect.defineMetadata(metadataKey, metadataValue, target, propertyKey);

// 检查元数据是否存在于对象的原型链或对象的属性上
let result = Reflect.hasMetadata(metadataKey, target);
let result = Reflect.hasMetadata(metadataKey, target, propertyKey);

// 检查元数据是否存在对象或对象的属性上
let result = Reflect.hasOwnMetadata(metadataKey, target);
let result = Reflect.hasOwnMetadata(metadataKey, target, propertyKey);

// 从对象的原型链或对象属性上获取元数据
let result = Reflect.getMetadata(metadataKey, target);
let result = Reflect.getMetadata(metadataKey, target, propertyKey);

//  从对象或对象属性上获取元数据
let result = Reflect.getOwnMetadata(metadataKey, target);
let result = Reflect.getOwnMetadata(metadataKey, target, propertyKey);

//  从对象的原型链或对象属性上获取所有元数据
let result = Reflect.getMetadataKeys(target);
let result = Reflect.getMetadataKeys(target, propertyKey);

// 从对象或对象属性上获取所有元数据
let result = Reflect.getOwnMetadataKeys(target);
let result = Reflect.getOwnMetadataKeys(target, propertyKey);

// 从对象或对象属性上删除元数据
let result = Reflect.deleteMetadata(metadataKey, target);
let result = Reflect.deleteMetadata(metadataKey, target, propertyKey);

// Reflect.metadata返回一个装饰器
// 通过@Reflect.metadata 装饰器给类添加元数据
@Reflect.metadata(metadataKey, metadataValue)
class C {
  //  通过@Reflect.metadata 装饰器给方法或属性添加元数据
  @Reflect.metadata(metadataKey, metadataValue)
  method() {
  }
}

内置元数据

  • 类型元信息: design:type
  • 参数类型元信息: design:paramtypes
  • 返回类型元信息: design:returntype
import 'reflect-metadata'

// 内置的三种元数据
function Type(type) {
  return Reflect.metadata('design:type', type)
}
function ParamTypes(...types) {
  return Reflect.metadata('design:paramtypes', types)
}
function ReturnType(type) {
  return Reflect.metadata('design:returntype', type)
}

// Decorator application
@ParamTypes(String, Number)
export class B {
  constructor(text, i) {}

  @Type(String)
  get name() {
    return 'text'
  }

  @Type(Function)
  @ParamTypes(Number, Number)
  @ReturnType(Number)
  add(x, y) {
    return x + y
  }
}

// Metadata introspection
let inst = new B('a', 1)

let constructorParamTypes = Reflect.getMetadata('design:paramtypes', B)
let type = Reflect.getMetadata('design:type', inst, 'add')
let paramTypes = Reflect.getMetadata('design:paramtypes', inst, 'add')
let returnType = Reflect.getMetadata('design:returntype', inst, 'add')

console.log(constructorParamTypes);
console.log(type);
console.log(paramTypes);
console.log(returnType);

// [ [Function: String], [Function: Number] ]
// [Function: Function]
// [ [Function: Number], [Function: Number] ]
// [Function: Number]

自定义元数据

除了使用类似design:type这种预定义的原信息外,我们也可以自定义信息。比如我们可以在删除用户的方法上添加一个角色判断,只有拥有我们设定角色的用户才能删除用户,比如管理员角色。

import 'reflect-metadata'

function GetName(target: Function): void;
function GetName(target: Object, propertyKey: string): void;
function GetName(target: Object | Function, propertyKey?: string){
  if (propertyKey) {
    console.log(Reflect.getMetadata('name', target, propertyKey))
  } else {
    console.log(Reflect.getMetadata('name', target))
  }
}

@GetName
// 通过@Reflect.metadata 装饰器给类添加元数据
@Reflect.metadata('name', 'A')
class A {

  @GetName
  //  通过@Reflect.metadata 装饰器给方法或属性添加元数据
  @Reflect.metadata('name', 'method')
  method() {}
}

new A().method()
// method
// A
转载自:https://juejin.cn/post/7210298403069444153
评论
请登录