TL;DR:滚动到底部查看完整的代码 sn-p。
使用装饰器添加可绑定属性并因此实现 composition 而不是 inheritance 是可能的,尽管不像人们想象的那么容易。以下是操作方法。
方法
假设我们有多个计算数字平方的组件。为此,需要两个属性:一个以基数作为输入(我们将此属性称为baseNumber),另一个提供计算结果(我们将此属性称为result)。 baseNumber-property 需要是可绑定的,这样我们才能传入一个值。result-property 需要依赖于baseNumber-property,因为如果输入发生变化,结果肯定会发生变化。
我们也不想在我们的属性中一遍又一遍地实现计算。我们也不能在这里使用继承,因为在编写本文时,在 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!
}
简单的解决方案
如果你只需要在你的类上放置一个可绑定的属性,事情就很简单了。您可以手动调用 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-property 提供的值计算的属性。为了正确实现,我们需要访问baseNumber-property 的值。现在,像我们的 addSquare-decorator 这样的装饰器不会在类的实例化期间评估,而是在类的声明期间评估。不幸的是,在这个阶段,根本没有我们可以从中读取所需值的实例。
(这并不妨碍我们首先使用bindable-decorator,因为这也是一个装饰器函数。因此它期望在类的声明期间应用并相应地实现。
Aurelia 中的 computedFrom-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);
完整的解决方案
要将所有内容整合在一起,我们需要完成几个步骤:
- 创建一个使用我们类的构造函数的装饰器函数
- 向类中添加一个名为
baseNumber 的可绑定属性
- 扩展构造函数以添加我们自己的计算属性
result
以下 sn-p 定义了一个名为 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;
}
我们完成了!你可以在这里看到整个过程:https://gist.run/?id=cc3207ee99822ab0adcdc514cfca7ed1
还有一件事
不幸的是,在运行时动态添加属性会破坏您的 TypeScript 开发体验。装饰器引入了两个新属性,但 TypeScript 编译器无法在编译时了解它们。
有人建议对 TypeScript 进行改进,以增强这种行为,但是在 GitHub 上,但这个建议远没有真正实现,因为这引入了一些有趣的问题和挑战。
因此,如果您需要从类的代码中访问新创建的属性之一,您始终可以将您的实例转换为 any:
let myVariable = (<any>this).baseNumber;
虽然这可行,但这既不是类型安全的,也不是很好看。通过更多的努力,您可以使代码看起来不错并且输入安全。您需要做的就是实现一个提供新属性的接口:
export interface IHasSquare {
baseNumber: number;
result: number;
}
只是将接口分配给我们的类是行不通的:记住,新创建的属性只在运行时存在。要使用该接口,我们可以在我们的类上实现一个返回this 的属性,但之前将其转换为IHasSquare。然而,为了诱使编译器允许这样做,我们需要先将this 转换为any:
get hasSquare(): IHasSquare {
return <IHasSquare>(<any>this);
}
感谢atsu85 指出将this 转换为它未实现的接口实际上可以工作!