如何通过装饰器向打字稿类添加可绑定属性或任何其他装饰器?

epi*_*tka 3 typescript aurelia typescript-decorator

我想使用装饰器而不是继承来扩展类的行为和数据。我还想将装饰器也应用于新创建的属性或方法。有没有如何做到这一点的例子?这甚至可能吗?

想象一组类,其中一些类共享一个名为 的可绑定属性span。还让有一个名为leftMargin依赖于该span属性的计算属性。实现这一点的理想方法是用一个名为@addSpanexample的装饰器来装饰类,该装饰器将可绑定属性和计算属性添加到类中。

Spo*_*xus 6

TL;DR:滚动到底部以获取完整的代码片段。

使用装饰器添加可绑定属性并因此实现组合而不是继承是可能的,尽管并不像人们想象的那么容易。这是如何做到的。

该方法

假设我们有多个组件来计算一个数的平方。为此,需要两个属性:一个以基数作为输入(我们称之为这个属性baseNumber),一个提供计算结果(我们称之为这个属性result)。该baseNumber-property需要被绑定的,所以我们可以传递一个值,result-property需要依赖于baseNumber-属性,因为如果输入的变化,肯定会的结果。

我们也不想在我们的属性中一遍又一遍地实现计算。我们也不能在这里使用继承,因为在撰写本文时,在 Aurelia 中继承可绑定和计算属性是不可能的。对于我们的应用程序架构来说,它可能也不是最好的选择。

所以最后我们想使用装饰器将请求的功能添加到我们的类中:

import { addSquare } from './add-square';

@addSquare
export class FooCustomElement {
  // FooCustomElement now should have
  // @bindable baseNumber: number;
  // @computedFrom('baseNumber') get result(): number {
  //   return this.baseNumber * this.baseNumber;  
  //}
  // without us even implementing it!
}
Run Code Online (Sandbox Code Playgroud)

简单的解决方案

如果你只需要在你的类上放置一个可绑定的属性,事情就很简单了。您可以bindable手动调用装饰器。这是有效的,因为在引擎盖下装饰器实际上只不过是函数。因此,要获得一个简单的可绑定属性,以下代码就足够了:

import { bindable } from 'aurelia-framework';

export function<T extends Function> addSquare(target: T) {
  bindable({
    name: 'baseNumber'
  })(target);
}
Run Code Online (Sandbox Code Playgroud)

这个对bindable-function 的调用添加了一个命名baseNumber为装饰类的属性。您可以像这样为属性分配或绑定一个值:

<foo base-number.bind="7"></foo>
<foo base-number="8"></foo>
Run Code Online (Sandbox Code Playgroud)

您当然也可以使用字符串插值语法来绑定以显示此属性的值:${baseNumber}

挑战

然而,挑战是添加另一个使用baseNumber-property提供的值计算的属性。为了正确实现,我们需要访问baseNumber-property的值。现在,像我们的addSquare-decorator这样的装饰器不在类的实例化期间进行评估,而是在类的声明期间进行评估。不幸的是,在这个阶段,根本没有我们可以从中读取所需值的实例。

(这并不妨碍我们首先使用bindable-decorator,因为这也是一个装饰器函数。因此它期望在类声明期间应用并相应地实现)。

computedFromAurelia 中的-decorator 是另一回事。我们不能像使用bindable-decorator那样使用它,因为它假定被装饰的属性已经存在于类实例中。

因此,从我们新创建的可绑定属性中实现计算属性似乎是一件非常不可能的事情,对吗?

幸运的是,有一种简单的方法可以从装饰器内部访问装饰类的实例:通过扩展其构造函数。在扩展构造函数中,我们可以添加一个计算属性,该属性可以访问我们装饰类的实例成员。

创建计算属性

在展示所有部分如何组合在一起之前,让我解释一下我们如何在其构造函数中手动向类添加计算属性:

// Define a property descriptor that has a getter that calculates the
// square number of the baseNumber-property.
let resultPropertyDescriptor = {
  get: () => {
    return this.baseNumber * this.baseNumber;
  }
}

// Define a property named 'result' on our object instance using the property
// descriptor we created previously.
Object.defineProperty(this, 'result', resultPropertyDescriptor);

// Finally tell aurelia that this property is being computed from the
// baseNumber property. For this we can manually invoke the function
// defining the computedFrom decorator. 
// The function accepts three arguments, but only the third one is actually 
// used in the decorator, so there's no need to pass the first two ones.
computedFrom('baseNumber')(undefined, undefined, resultPropertyDescriptor);
Run Code Online (Sandbox Code Playgroud)

完整的解决方案

要将所有内容整合在一起,我们需要完成以下几个步骤:

  • 创建一个装饰器函数,它接受我们类的构造函数
  • 添加一个命名baseNumber为类的可绑定属性
  • 扩展构造函数以添加我们自己的名为的计算属性 result

以下代码段定义了一个addSquare满足上述要求的装饰器:

import { bindable, computedFrom } from 'aurelia-framework';

export function addSquare<TConstructor extends Function>(target: TConstructor) {

  // Store the original target for later use
  var original = target;

  // Define a helper function that helps us to extend the constructor
  // of the decorated class.
  function construct(constructor, args) {

    // This actually extends the constructor, by adding new behavior
    // before invoking the original constructor with passing the current
    // scope into it.
    var extendedConstructor: any = function() {

      // Here's the code for adding a computed property
      let resultPropertyDescriptor = {
        get: () => {
          return this.baseNumber * this.baseNumber;
        }
      }
      Object.defineProperty(this, 'result', resultPropertyDescriptor);
      computedFrom('baseNumber')(target, 'result', resultPropertyDescriptor);

      // Here we invoke the old constructor.
      return constructor.apply(this, args);
    }

    // Do not forget to set the prototype of the extended constructor
    // to the original one, because otherwise we would miss properties
    // of the original class.
    extendedConstructor.prototype = constructor.prototype;

    // Invoke the new constructor and return the value. Mind you: We're still
    // inside a helper function. This code won't get executed until the real
    // instanciation of the class!
    return new extendedConstructor();
  }

  // Now create a function that invokes our helper function, by passing the
  // original constructor and its arguments into it.
  var newConstructor: any = function(...args) {
    return construct(original, args);
  }

  // And again make sure the prototype is being set correctly.
  newConstructor.prototype = original.prototype;

  // Now we add the bindable property to the newly created class, much
  // as we would do it by writing @bindinable on a property in the definition
  // of the class.
  bindable({
    name: 'baseNumber',
  })(newConstructor);

  // Our directive returns the new constructor so instead of invoking the
  // original one, javascript will now use the extended one and thus enrich
  // the object with our desired new properties.
  return newConstructor;
}
Run Code Online (Sandbox Code Playgroud)

我们完成了!你可以在这里看到整个过程:https : //gist.run/?id=cc3207ee99822ab0adcdc514cfca7ed1

还有一件事

不幸的是,在运行时动态添加属性会破坏您的 TypeScript 开发体验。装饰器引入了两个新属性,但 TypeScript 编译器在编译时无法知道它们。有人建议对 TypeScript 进行改进,但在 GitHub 上增强了这种行为,但是这个建议远未真正实现,因为这引入了一些有趣的问题和挑战。因此,如果您需要从类的代码中访问新创建的属性之一,您可以始终将实例转换为any

let myVariable = (<any>this).baseNumber;
Run Code Online (Sandbox Code Playgroud)

虽然这有效,但这既不是类型安全的,也不好看。稍加努力,您既可以使代码看起来更美观,又可以使类型安全。您需要做的就是实现一个提供新属性的接口:

export interface IHasSquare {
    baseNumber: number;
    result: number;
}       
Run Code Online (Sandbox Code Playgroud)

但是,简单地将接口分配给我们的类是行不通的:请记住,新创建的属性仅在运行时存在。要使用该接口,我们可以在我们的类上实现一个属性,该属性返回this,但之前将其强制转换为IHasSquare. 但是,为了欺骗编译成允许这一点,我们需要转换thisany第一但是:

get hasSquare(): IHasSquare {
    return <IHasSquare>(<any>this);
}
Run Code Online (Sandbox Code Playgroud)

荣誉对atsu85铸造指出this一个接口它实际上并没有实现可以工作!