如何实现打字稿装饰器?

How to implement a typescript decorator?

TypeScript 1.5 now has decorators.

有人可以提供一个简单示例来演示实现装饰器的正确方法并描述可能的有效装饰器签名中的参数的含义吗?

declare type ClassDecorator = <TFunction extends Function>(target: TFunction) => TFunction | void;
declare type PropertyDecorator = (target: Object, propertyKey: string | symbol) => void;
declare type MethodDecorator = <T>(target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T> | void;
declare type ParameterDecorator = (target: Function, propertyKey: string | symbol, parameterIndex: number) => void;

此外,在实施装饰器时是否应牢记最佳实践注意事项?

class Foo {
  @consoleLogger 
  Boo(name:string) { return "Hello, " + name }
}
  • target:class 的原型,在上面的例子中是 "Foo"
  • 属性Key:调用的方法名,在上面的例子中"Boo"
  • descriptor:对象的描述=> 包含值属性,它又是函数本身:function(name) { return 'Hello' + name; }

您可以实现一些功能来记录对控制台的每次调用:

function consoleLogger(target: Function, key:string, value:any) 
{
  return value: (...args: any[]) => 
  {
     var a = args.map(a => JSON.stringify(a)).join();
     var result = value.value.apply(this, args);
     var r = JSON.stringify(result);

     console.log('called method' + key + ' with args ' + a + ' returned result ' + r);

     return result;
  }     
}

我最终开始使用装饰器,并决定在任何文档发布之前为任何想要利用它的人记录我的发现。如果您发现任何错误,请随时编辑。

一般积分

  • 在声明 class 时调用装饰器,而不是在实例化对象时调用。
  • 同一个装饰器可以定义多个Class/Property/Method/Parameter.
  • 构造函数上不允许装饰器。

A valid decorator should be:

  1. Assignable to one of the Decorator types (ClassDecorator | PropertyDecorator | MethodDecorator | ParameterDecorator).
  2. Return a value (in the case of class decorators and method decorator) that is assignable to the decorated value.

Reference


方法/正式访问装饰器

执行参数:

  • target:class(Object)的原型。
  • propertyKey: 方法名(string | symbol).
  • descriptor: A TypedPropertyDescriptor — If you're unfamiliar with a descriptor's keys, I would recommend reading about it in this documentation on Object.defineProperty (这是第三个参数).

示例 - 没有参数

使用:

class MyClass {
    @log
    myMethod(arg: string) { 
        return "Message -- " + arg;
    }
}

实施:

function log(target: Object, propertyKey: string, descriptor: TypedPropertyDescriptor<any>) {
    const originalMethod = descriptor.value; // save a reference to the original method

    // NOTE: Do not use arrow syntax here. Use a function expression in 
    // order to use the correct value of `this` in this method (see notes below)
    descriptor.value = function(...args: any[]) {
        // pre
        console.log("The method args are: " + JSON.stringify(args));
        // run and store result
        const result = originalMethod.apply(this, args);
        // post
        console.log("The return value is: " + result);
        // return the result of the original method (or modify it before returning)
        return result;
    };

    return descriptor;
}

输入:

new MyClass().myMethod("testing");

输出:

The method args are: ["testing"]

The return value is: Message -- testing

备注:

  • 设置描述符的值时不要使用箭头语法。
  • 修改原始描述符比通过return使用新描述符覆盖当前描述符更好。这允许您使用多个装饰器来编辑描述符而不会覆盖另一个装饰器所做的。这样做可以让您同时使用 @enumerable(false)@log 之类的东西(示例:Bad vs Good
  • 有用: TypedPropertyDescriptor的类型参数可以用来限制装饰器可以放在什么方法签名(Method Example) or accessor signatures (Accessor Example)上。

示例 - 带参数(装饰器工厂)

使用参数时,您必须声明一个带有装饰器参数的函数,然后 return 一个带有示例签名但不带参数的函数。

class MyClass {
    @enumerable(false)
    get prop() {
        return true;
    }
}

function enumerable(isEnumerable: boolean) {
    return (target: Object, propertyKey: string, descriptor: TypedPropertyDescriptor<any>) => {
        descriptor.enumerable = isEnumerable;
        return descriptor;
    };
}

静态方法装饰器

类似于方法装饰器,但有一些区别:

  • 它的target参数是构造函数本身而不是原型。
  • 描述符是在构造函数而不是原型上定义的。

Class装饰器

@isTestable
class MyClass {}

实现参数:

  • target: class 装饰器声明于 (TFunction extends Function).

Example use:使用元数据 api 存储关于 class 的信息。


属性装饰器

class MyClass {
    @serialize
    name: string;
}

执行参数:

  • target: class(Object)的原型.
  • propertyKey: 属性 的名称 (string | symbol).

: 创建一个 @serialize("serializedName") 装饰器并将 属性 名称添加到要序列化的属性列表中。


参数装饰器

class MyClass {
    myMethod(@myDecorator myParameter: string) {}
}

执行参数:

  • target:class的原型(Function——看来Function已经不行了,你应该用any或者Object 现在在这里以便在任何 class 中使用装饰器。或者指定要将其限制为的 class 类型)
  • propertyKey: 方法名(string | symbol).
  • parameterIndex: 参数在函数参数列表中的索引(number).

Simple example

详细示例

我在其他答案中没有看到的一件重要事情:

装饰器工厂

If we want to customize how a decorator is applied to a declaration, we can write a decorator factory. A Decorator Factory is simply a function that returns the expression that will be called by the decorator at runtime.

// This is a factory, returns one of ClassDecorator,
// PropertyDecorator, MethodDecorator, ParameterDecorator
function Entity(discriminator: string):  {
    return function(target) {
        // this is the decorator, in this case ClassDecorator.
    }
}

@Entity("cust")
export class MyCustomer { ... }

查看 TypeScript 手册 Decorators chapter

TS 装饰器:

TS 装饰器允许在 class 上添加额外的功能。 class 在 声明时间 被装饰器更改,然后再创建 class 的任何实例。

语法:

装饰器使用 @ 符号声明,例如 @metadata。 TS 现在将搜索相应的元数据函数,并自动为其提供几个参数,这些参数根据装饰的内容而有所不同(例如 class 或 class 属性 得到不同的参数)

装饰器函数中提供了这些参数:

  • class
  • 的原型对象
  • 属性键或方法名
  • 属性描述符对象,看起来像这样 {writable: true, enumerable: false, configurable: true, value: ƒ}

根据装饰器的类型,这些参数中的 1-3 个被传递给装饰器函数。

装饰器类型:

以下装饰器可以应用于 class 并且 TS 将按以下顺序评估它们(以下总和来自 TS 文档):

  1. 参数装饰器,后跟方法、访问器或 属性 装饰器应用于每个实例成员。
  2. 参数装饰器,后跟方法、访问器或 属性 为每个静态成员应用装饰器。
  3. 参数装饰器应用于构造函数。
  4. Class装饰器应用于class

更好地理解它们的最佳方式是通过示例。请注意,这些示例确实需要对 TS 语言和 PropertyDescriptor.

等概念有深入的了解

方法装饰器:

function overwrite(
    target: myClass,
    propertyKey: string,
    descriptor: PropertyDescriptor
) {
    console.log('I get logged when the class is declared!')

    // desciptor.value refers to the actual function fo the class
    // we are changing it to another function which straight up 
    // overrides the other function
    descriptor.value = function () {
        return 'newValue method overwritten'
    }
}

function enhance(
    target: myClass,
    propertyKey: string,
    descriptor: PropertyDescriptor
) {
    const oldFunc = descriptor.value;

    // desciptor.value refers to the actual function fo the class
    // we are changing it to another function which calls the old
    // function and does some extra stuff
    descriptor.value = function (...args: any[]) {
        console.log('log before');
        const returnValue = oldFunc.apply(this, args)
        console.log('log after');

        return returnValue;
    }
}


class myClass {

    // here is the decorator applied
    @overwrite
    foo() {
        return 'oldValue';
    }

    // here is the decorator applied
    @enhance
    bar() {
        return 'oldValueBar';
    }

}

const instance =new myClass()

console.log(instance.foo())
console.log(instance.bar())

// The following gets logged in this order:

//I get logged when the class is declared!
// newValue method overwritten
// log before
// log after
// oldValueBar

属性 装饰器:

function metaData(
    target: myClass,
    propertyKey: string,
    // A Property Descriptor is not provided as an argument to a property decorator due to 
    // how property decorators are initialized in TypeScript.
) {

    console.log('Execute your custom code here')
    console.log(propertyKey)

}

class myClass {

    @metaData
    foo = 5

}


// The following gets logged in this order:

// Execute your custom code here
// foo

Class 装饰器(来自 TS 文档):

function seal(
    constructor: Function,
) {

    // Object.seal() does the following:
    // Prevents the modification of attributes of 
    // existing properties, and prevents the addition 
    // of new properties
    Object.seal(constructor);
    Object.seal(constructor.prototype);

}

@seal
class myClass {

    bar?: any;
    
    foo = 5

}
 
myClass.prototype.bar = 10;

// The following error will be thrown:

// Uncaught TypeError: Cannot add property bar,
// object is not extensible
 

装饰器和装饰器工厂:

装饰器可以通过装饰器函数或装饰器工厂函数声明。语法上存在差异,最好通过示例进行解释:

// Returns a decorator function, we can return any function
// based on argument if we want
function decoratorFactory(arg: string) {
    return function decorator(
    target: myClass,
    propertyKey: string,
) {
    console.log(`Log arg ${arg} in decorator factory`);
}
}

// Define a decorator function directly
function decorator(
    target: myClass,
    propertyKey: string,
) {
    console.log('Standard argument');
}

class myClass {

    // Note the parentheses and optional arguments 
    // in the decorator factory
    @decoratorFactory('myArgument')
    foo = 'foo';

    // No parentheses or arguments
    @decorator
    bar = 'bar';

}


// The following gets logged in this order:

// Log arg myArgument in decorator factory
// Standard argument

您还可以 decorate/enhance 打字稿中原始构造函数的新功能(我使用的是 3.9.7)。下面的代码片段包装了原始构造函数以添加名称 属性 的前缀。当 class 是 instantiated 而不是 class 是 declared!

时会发生这种情况
 //Decorator function
 function Prefixer(prefix: string) { 
    return function<T extends { new (...args: any[]): {name: string} }>(
      originalCtor: T
    ) {
      return class extends originalCtor {
        constructor(..._: any[]) {
          super();
          this.name = `${prefix}.${this.name.toUpperCase()}`;        
          console.log(this.name);       
        }
      };
    };
  }

当 class 为 instantiated 时,新的构造函数逻辑与原始构造函数逻辑一起运行 -

  @Prefixer('Mr')
  class Person {
    name = 'MBB';
  
    constructor() {
      console.log('original ctr logic here!');
    }
  }
  
  const pers = new Person();
  
  console.log(pers); //Mr.MBB