如何通过装饰器将可绑定属性或任何其他装饰器添加到Typescript类? [英] How to add bindable attributes or any other decorators to a typescript class via decorator?

查看:91
本文介绍了如何通过装饰器将可绑定属性或任何其他装饰器添加到Typescript类?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我想使用装饰器而不是继承来扩展类的行为和数据.我还想将装饰器也应用于新创建的属性或方法.有一个如何执行此操作的示例吗?这有可能吗?

I want to extend the behavior and data of a class using decorators instead of inheritance. I also want to apply decorators as well to the newly created properties or methods. Is there an example of how to do this? Is this even possible?

想象一下一组类,其中的某些类共享一个名为span的可绑定属性.还让一个名为leftMargin的计算属性取决于span属性.实现此目标的理想方法是,例如,使用名为@addSpan的装饰器装饰类,该装饰器将可绑定属性和计算的属性都添加到该类中.

Imagine a set of classes where some of these classes share a bindable property named span. Also let there be a computed property named leftMargin dependent on the span property. The desired way to implement this would be to decorate the class with a decorator named @addSpan for example, that adds both the bindable property and the computed property to the class.

推荐答案

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

可以使用装饰器添加可绑定属性,从而实现 composition 而不是 inheritance ,尽管这并不像人们想象的那么容易.这是这样做的方法.

Adding bindable properties using a decorator and thus implementing composition instead of inheritance is possible, though not as easy as one might guess. Here's how to do it.

让我们想象一下,我们有多个组件可以计算数字的平方.为此,需要两个属性:一个以基数为输入(我们将其称为属性baseNumber),另一个提供计算结果(将其称为属性result). baseNumber属性必须是可绑定的,以便我们可以传入一个值.result属性需要依赖于baseNumber属性,因为如果输入发生更改,则肯定会得到结果.

Lets imagine we have multiple components that calculate the square of a number. For this two properties are required: One taking the base number as input (we'll call this property baseNumber) and one providing the result of the calculation (let's call this property result). The baseNumber-property needs to be bindable so we can pass a value in. The result-property needs to depend on the baseNumber-property, because if the input changes, for sure will the result.

我们也不希望在属性中一遍又一遍地执行计算.我们在这里也不能使用继承,因为在编写本文时,无法在Aurelia中继承可绑定和计算的属性.对于我们的应用程序体系结构而言,这也不是最好的选择.

Neither do we want to implement the calculation over and over again in our properties. Nor can we use inheritance here, because inheriting bindable and computed properties in Aurelia is not possible at the time of writing this. It might not be the best of the bets for our application architecture, too.

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

So in the end we would like to use a decorator to add the requested functionality to our class:

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装饰器.那行得通,因为在内部,装饰器实际上只不过是功能.因此,要获得一个简单的可绑定属性,以下代码就足够了:

The Simple Solution

If you need to place just a bindable property on your class, things are simple. You can just invoke the bindable decorator manually. That works, because under the hood decorators are nothing more than functions actually. So to get a simple bindable property the following code is enough:

import { bindable } from 'aurelia-framework';

export function<T extends Function> addSquare(target: T) {
  bindable({
    name: 'baseNumber'
  })(target);
}

此对bindable函数的调用将一个名为baseNumber的属性添加到装饰的类中.您可以像这样将值分配或绑定到属性:

This call to the bindable-function adds a property named baseNumber to the decorated class. You can assign or bind a value to the property just like this:

<foo base-number.bind="7"></foo>
<foo base-number="8"></foo>

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

You can of course also use the string interpolation syntax to bind to display this property's value: ${baseNumber}.

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

The challenge however is to add another property that is computed using the value provided by the baseNumber-property. For a proper implementation we need to access the value of the baseNumber-property. Now decorators like our addSquare-decorator are not evaluated during instanciation of a class, but rather during the declaration of a class. Unfortunately during this phase there simply is no instance we could possibly read the desired value from.

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

(This does not hinder us to use the bindable-decorator in first place, because this also is a decorator function. Thus it expects to be applied during declaration of a class and is implemented accordingly).

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

The computedFrom-decorator in Aurelia is a different matter. We cannot use it the same way as we did with the bindable-decorator, because it assumes that the decorated property already exists on the class instance.

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

So implementing a computed property from our newly created bindable one seems to be a pretty impossibly thing right?

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

Well, luckily there is an easy way to access the instance of a decorated class from within the decorator: By extending its constructor function. In the extended constructor we can then add a computed property that has access to instance-members of our decorated class.

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

Before showing how all parts fit together let me explain how we can manually add a computed property to a class in its constructor:

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

完整解决方案

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

The Complete Solution

To bring everything together we need to accomplish several steps:

  • 创建一个装饰器函数,该函数采用我们类的构造器
  • 将名为baseNumber的可绑定属性添加到类
  • 扩展构造函数以添加我们自己的名为result
  • 的计算属性
  • Create a decorator function that takes the constructor of our class
  • Add a bindable property named baseNumber to the class
  • Extend the constructor to add our own computed property named result

以下代码段定义了一个装饰器,其名称为addSquare,可以满足上述要求:

The following snippet defines a decorator named addSquare that fulfills the requirements stated above:

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;
}

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

And we're done! You can see the whole thing in action here: https://gist.run/?id=cc3207ee99822ab0adcdc514cfca7ed1

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

Adding properties dynamically at runtime will unfortunately break your TypeScript development experience. The decorator introduces two new properties, but the TypeScript compiler has no means to know about them at compiletime. Someone suggested an improvement to TypeScript enhancing this behavior however over at GitHub, but this suggestions is far from being actually implemented, because this introduces quite some interesting questions and challenges. So should you need to access one of the newly created properties from the code of your class you could always cast your instance to any:

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

虽然可行,但这既不是类型安全的,也不是很好看的.稍加努力,您既可以使代码看起来不错,也可以键入安全的代码.您需要做的就是实现一个提供新属性的接口:

While this works, this is neither type safe nor does it look nice. With a bit more effort you could both make the code look nice and type safe. All you need to do is to implement an interface providing the new properties:

export interface IHasSquare {
    baseNumber: number;
    result: number;
}       

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

Simply assigning the interface to our class won't work though: remember, the newly created properties only exist at runtime. To use the interface we can implement a property on our class that returns this, but previously cast it to IHasSquare. To trick the compiler however into allowing this, we need to cast this to any first however:

get hasSquare(): IHasSquare {
    return <IHasSquare>(<any>this);
}

atsu85 表示感谢,指出将this强制转换为实际上未实现的接口是可以的!

Kudos to atsu85 for pointing out casting this to an interface it does not implement actually can work!

这篇关于如何通过装饰器将可绑定属性或任何其他装饰器添加到Typescript类?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆