如何通过装饰器将可绑定属性或任何其他装饰器添加到typescript类?

时间:2017-05-11 14:47:17

标签: typescript aurelia aurelia-binding typescript-decorator

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

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

1 个答案:

答案 0 :(得分:3)

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

使用装饰器添加可绑定属性,从而实现组合而不是继承是可能的,尽管不像人们猜测的那么容易。这是怎么做的。

方法

让我们假设我们有多个组件来计算数字的平方。为此,需要两个属性:一个将基数作为输入(我们将调用此属性baseNumber),另一个提供计算结果(让我们调用此属性{{1} })。 result - 属性需要是可绑定的,因此我们可以传入一个值。baseNumber - 属性需要依赖于result - 属性,因为如果输入发生了变化,肯定会结果。

我们也不想在我们的属性中反复实施计算。我们也不能在这里使用继承,因为在写这篇文章的时候,继承Aurelia中的可绑定和计算属性是不可能的。它也可能不是我们的应用程序架构中最好的选择。

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

baseNumber

简单解决方案

如果您只需要在类上放置一个可绑定属性,那么事情很简单。您可以手动调用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! } 装饰器。这是有效的,因为在引擎盖下装饰器只不过是功能。因此,要获得一个简单的可绑定属性,以下代码就足够了:

bindable

import { bindable } from 'aurelia-framework'; export function<T extends Function> addSquare(target: T) { bindable({ name: 'baseNumber' })(target); } - 函数的调用将一个名为bindable的属性添加到已修饰的类中。您可以将值分配或绑定到属性,如下所示:

baseNumber

您当然也可以使用字符串插值语法进行绑定以显示此属性的值:<foo base-number.bind="7"></foo> <foo base-number="8"></foo>

挑战

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

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

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

所以从我们新创建的可绑定的实现计算属性似乎是一个非常不可能的事情吗?

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

创建计算属性

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

bindable

完整解决方案

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

  • 创建一个装饰器函数,它接受我们类的构造函数
  • 将一个名为// 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); 的可绑定属性添加到类
  • 扩展构造函数以添加我们自己的名为baseNumber
  • 的计算属性

以下代码段定义了一个名为result的装饰器,它符合上述要求:

addSquare

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

还有一件事

不幸的是,在运行时动态添加属性会破坏您的TypeScript开发体验。装饰器引入了两个新属性,但TypeScript编译器无法在编译时了解它们。 有人建议改进TypeScript,但是在GitHub上增强了这种行为,但是这个建议远未实际实现,因为这引入了一些有趣的问题和挑战。 因此,如果您需要从班级代码中访问其中一个新创建的属性,您始终可以将实例强制转换为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; }

any

虽然这有效,但这既不是类型安全也不是很好看。通过更多努力,您可以使代码看起来更好并且类型安全。您需要做的就是实现提供新属性的接口:

let myVariable = (<any>this).baseNumber;

简单地将接口分配给我们的类仍然无法工作:记住,新创建的属性仅存在于运行时。要使用该接口,我们可以在返回export interface IHasSquare { baseNumber: number; result: number; } 的类上实现一个属性,但之前将其转换为this。但是为了欺骗编译器允许这样做,我们需要首先将IHasSquare强制转换为this

any

感谢atsu85指出将get hasSquare(): IHasSquare { return <IHasSquare>(<any>this); } 投射到实际未实现的界面可以正常工作!