【问题标题】:How to implement a typescript decorator?如何实现打字稿装饰器?
【发布时间】:2015-06-28 19:52:21
【问题描述】:

TypeScript 1.5 现在有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;

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

【问题讨论】:

标签: typescript decorator


【解决方案1】:

我最终玩弄了装饰器,并决定在任何文档出来之前记录下我的发现,以供任何想要利用它的人使用。如果您发现任何错误,请随时编辑。

一般要点

  • 在声明类时调用装饰器,而不是在实例化对象时调用。
  • 可以在同一个类/属性/方法/参数上定义多个装饰器。
  • 构造函数中不允许使用装饰器。

一个有效的装饰器应该是:

  1. 可分配给装饰器类型之一 (ClassDecorator | PropertyDecorator | MethodDecorator | ParameterDecorator)。
  2. 返回一个可分配给装饰值的值(在类装饰器和方法装饰器的情况下)。

Reference


方法/形式访问器装饰器

实现参数:

  • target:类的原型(Object)。
  • propertyKey:方法的名称 (string | symbol)。
  • descriptor: A TypedPropertyDescriptor — 如果您不熟悉描述符的键,我建议您在 Object.defineProperty 上的 this documentation 中阅读它(这是第三个参数)。

示例 - 不带参数

用途:

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");

输出:

方法参数为:["testing"]

返回值为:Message -- testing

注意事项:

  • 在设置描述符的值时不要使用箭头语法。 The context of this will not be the instance's if you do.
  • 修改原始描述符比通过返回新描述符覆盖当前描述符要好。这允许您使用多个装饰器来编辑描述符,而不会覆盖另一个装饰器所做的事情。这样做可以让您同时使用 @enumerable(false)@log 之类的东西(例如:BadGood
  • 有用TypedPropertyDescriptor 的 type 参数可用于限制装饰器可以放在哪些方法签名 (Method Example) 或访问器签名 (Accessor Example) 上。

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

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

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 参数是构造函数本身,而不是原型。
  • 描述符是在构造函数而不是原型上定义的。

类装饰器

@isTestable
class MyClass {}

实现参数:

  • target:声明装饰器的类 (TFunction extends Function)。

Example use:使用元数据 api 来存储类的信息。


属性装饰器

class MyClass {
    @serialize
    name: string;
}

实现参数:

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

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


参数装饰器

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

实现参数:

  • target:类的原型(Function——似乎Function 不再起作用了。您现在应该在这里使用anyObject,以便在任何类中使用装饰器。或者指定您希望将其限制为的类类型)
  • propertyKey:方法的名称 (string | symbol)。
  • parameterIndex:函数参数列表中参数的索引(number)。

Simple example

详细示例

【讨论】:

  • 你知道在哪里可以找到参数装饰器的例子吗?我一直在尝试实现一个没有成功github.com/Microsoft/TypeScript/issues/…
  • @OweRReLoaDeD 我在参数装饰器下添加了一个示例,它只是注销了传递给装饰器的内容。我不确定这是否有帮助。目前我想不出一个好的例子。
  • 仅供参考,我在 github 上收集并调整了这些信息:github.com/arolson101/typescript-decorators
  • --experimentalDecorators 标志必须设置才能使这个例子工作
  • 我对@9​​87654377@ 或prototype of the classkey 指的是什么有点困惑,有人可以详细说明一下吗?
【解决方案2】:

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

装饰厂

如果我们想自定义如何将装饰器应用于声明,我们可以编写一个装饰器工厂。装饰器工厂只是一个函数,它返回装饰器在运行时将调用的表达式。

// 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

【讨论】:

    【解决方案3】:
    class Foo {
      @consoleLogger 
      Boo(name:string) { return "Hello, " + name }
    }
    
    • target:上面例子中类的原型是“Foo”
    • propertyKey:被调用方法的名称,在上面的例子中是“Boo”
    • descriptor: description of object => 包含 value 属性,这又是函数本身: 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;
      }     
    }
    

    【讨论】:

    • 使用严格的编译器设置进行编译是一项艰巨的任务
    • 其实这是错误的,不能编译,需要在return { value : ...}后面直接加花括号。这甚至可以从您的代码的潜在来源中看出 - blog.wolksoftware.com/…
    【解决方案4】:

    TS 装饰器:

    TS 装饰器允许在类上添加额外的功能。在创建类的任何实例之前,装饰器会在声明时间更改类。

    语法:

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

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

    • 类的原型对象
    • 属性键或方法名称
    • PropertyDescriptor 对象,如下所示 {writable: true, enumerable: false, configurable: true, value: ƒ}

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

    装饰器类型:

    以下装饰器可以应用于一个类,TS 将按以下顺序评估它们(以下总结来自 TS 文档):

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

    更好地理解它们的最佳方法是通过示例。请注意,这些示例确实需要对 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
    

    类装饰器(来自 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
    

    【讨论】:

      【解决方案5】:

      您还可以 decorate/enhance 为 typescript 中的原始构造函数添加新功能(我使用的是 3.9.7)。下面的 sn-p 包装了原始构造函数,为 name 属性添加前缀。当班级为instantiated 而不是班级为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);       
              }
            };
          };
        }
      

      当类为 instantiated 时,新的构造函数逻辑使用原始 ctor 逻辑运行 -

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

      【讨论】:

        猜你喜欢
        • 2021-09-30
        • 2018-06-21
        • 2019-07-29
        • 2016-05-08
        • 1970-01-01
        • 1970-01-01
        • 2020-05-31
        • 2020-01-02
        • 2020-01-31
        相关资源
        最近更新 更多