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

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

问题描述

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

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:滚动到底部查看完整的代码片段.

使用装饰器添加可绑定属性,从而实现组合而不是继承是可能的,尽管并不像人们想象的那么容易.这是操作方法.

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-property 需要可绑定,以便我们可以传入一个值. result-property 需要依赖于 baseNumber-property,因为如果输入改变,结果肯定会改变.

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 属性的值.现在,像我们的 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 装饰器,因为这也是一个装饰器函数.因此它期望在类的声明期间应用并相应地实现).

(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 装饰器是另一回事.我们不能像使用 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 编译器在编译时无法知道它们.有人建议对 TypeScript 进行改进,但在 GitHub 上增强了这种行为,但是这个建议远未真正实现,因为这引入了一些有趣的问题和挑战.因此,如果您需要从类的代码中访问新创建的属性之一,您可以随时将实例转换为 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!

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

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