Mixin 作为 TypeScript 中的 class 装饰器不会更新 class 属性
Mixin as class decorator in TypeScript does not update class property
简介
我在 TS 中有一个项目需要一些 classes 来实现以下接口:
interface IStylable {
readonly styles: {
[property: string]: string
};
addStyles (styles: { [property: string]: string }): void;
updateStyles (styles: { [property: string]: string }): void;
removeStyles (styles: Array<string>): void;
}
为了避免样板代码,我决定创建一个 Mixin 并将其应用到我需要的每个 class 中。 (我可以使用抽象 class 但我的问题需要多重继承解决方案,TS 不提供。)下面是 IStylable
接口的 class 实现:
export class StylableClass implements IStylable {
private readonly _styles: { [property: string]: string } = {};
// For each property provided in styles param, check if the property
// is not already present in this._styles and add it. This way we
// do not overide existing property values.
public addStyles (styles: { [property: string]: string }): void {
for (const [property, value] of Object.entries(styles)) {
if (!this._styles.hasOwnProperty(property)) {
this._styles[property] = value;
}
}
}
// For each property provided in styles param, check if the property
// is already present in this._styles and add it. This way we
// do add property values values that do not exist.
public updateStyles (styles: { [property: string]: string }): void {
for (const [property, value] of Object.entries(styles)) {
if (this._styles.hasOwnProperty(property)) {
this._styles[property] = value;
}
}
}
// For each property in styles param, check if it is present in this._styles
// and remove it.
public removeStyles (styles: Array<string>): void {
for (const property of styles) {
if (this._styles.hasOwnProperty(property)) {
delete this._styles[property];
}
}
}
public set styles (styles: { [property: string]: string }) {
this.addStyles(styles);
}
public get styles (): { [property: string]: string } {
return this._styles;
}
}
让我真正兴奋和期待的是 ES6 中装饰器规范的标准化。 Typescript 通过在 tsconfig.json
中设置 experimentalDecorators
标志来允许此实验性功能。我希望 StylableClass
用作 class 装饰器 (@Stylable
) 以使代码更清晰,因此我创建了一个函数,该函数接受 class 并将其转换为装饰器:
export function makeDecorator (decorator: Function) {
return function (decorated: Function) {
const fieldCollector: { [key: string]: string } = {};
decorator.apply(fieldCollector);
Object.getOwnPropertyNames(fieldCollector).forEach((name) => {
decorated.prototype[name] = fieldCollector[name];
});
Object.getOwnPropertyNames(decorator.prototype).forEach((name) => {
decorated.prototype[name] = decorator.prototype[name];
});
};
}
并像这样使用它:
export const Stylable = () => makeDecorator(StylableClass);
问题
现在是单元测试的时候了。我创建了一个虚拟 class 来应用我的装饰器并为 addStyles()
方法编写了一个简单的测试。
@Stylable()
class StylableTest {
// Stylable
public addStyles!: (styles: {
[prop: string]: string;
}) => void;
public updateStyles!: (styles: {
[prop: string]: string;
}) => void;
public removeStyles!: (styles: string[]) => void;
public styles: { [property: string]: string } = {};
}
describe('Test Stylable mixin', () => {
it('should add styles', () => {
const styles1 = {
float: 'left',
color: '#000'
};
const styles2 = {
background: '#fff',
width: '100px'
};
// 1
const styles = new StylableTest();
expect(styles.styles).to.be.an('object').that.is.empty;
// 2
styles.addStyles(styles1);
expect(styles.styles).to.eql(styles1);
// 3
styles.addStyles(styles2);
expect(styles.styles).to.eql(Object.assign({}, styles1, styles2));
});
});
问题是第二个 expect 语句失败了。在执行 styles.addStyles(styles1);
之后,styles.styles
数组应该包含 styles1
对象时仍然是空的。当我调试我的代码时,我发现 push
方法中的 addStyles()
语句按预期执行,因此循环没有问题,但是方法执行结束后数组没有更新。您能否就我遗漏的内容提供提示或解决方案?我检查的第一件事是 makeDecorator
函数可能出了问题,但只要我可以执行这些方法,我就找不到其他线索来寻找。
StylableClass
mixin 声明了一个名为 styles
的 属性。但是 StylableTest
创建了一个字段名称 styles
并为其分配了一个没有人会使用的空对象。您需要将 属性 描述从装饰器转移到目标 class,并从 StylableTest
中的 styles
中删除 = {}
:
function makeDecorator(decorator) {
return function (decorated) {
var fieldCollector = {};
decorator.apply(fieldCollector);
Object.getOwnPropertyNames(fieldCollector).forEach(function (name) {
decorated.prototype[name] = fieldCollector[name];
});
Object.getOwnPropertyNames(decorator.prototype).forEach(function (name) {
var descriptor = Object.getOwnPropertyDescriptor(decorator.prototype, name);
if (descriptor) {
Object.defineProperty(decorated.prototype, name, descriptor);
}
else {
decorated.prototype[name] = decorator.prototype[name];
}
});
};
}
我可以建议 less error prone approach 在打字稿中混入。这种必须重新声明所有 mixin 成员的方法将在以后导致错误。至少避免使用类型查询重述字段的类型:
@Stylable()
class StylableTest {
// Stylable
public addStyles!: IStylable['addStyles']
public updateStyles!: IStylable['updateStyles']
public removeStyles!: IStylable['removeStyles']
public styles!: IStylable['styles']
}
简介
我在 TS 中有一个项目需要一些 classes 来实现以下接口:
interface IStylable {
readonly styles: {
[property: string]: string
};
addStyles (styles: { [property: string]: string }): void;
updateStyles (styles: { [property: string]: string }): void;
removeStyles (styles: Array<string>): void;
}
为了避免样板代码,我决定创建一个 Mixin 并将其应用到我需要的每个 class 中。 (我可以使用抽象 class 但我的问题需要多重继承解决方案,TS 不提供。)下面是 IStylable
接口的 class 实现:
export class StylableClass implements IStylable {
private readonly _styles: { [property: string]: string } = {};
// For each property provided in styles param, check if the property
// is not already present in this._styles and add it. This way we
// do not overide existing property values.
public addStyles (styles: { [property: string]: string }): void {
for (const [property, value] of Object.entries(styles)) {
if (!this._styles.hasOwnProperty(property)) {
this._styles[property] = value;
}
}
}
// For each property provided in styles param, check if the property
// is already present in this._styles and add it. This way we
// do add property values values that do not exist.
public updateStyles (styles: { [property: string]: string }): void {
for (const [property, value] of Object.entries(styles)) {
if (this._styles.hasOwnProperty(property)) {
this._styles[property] = value;
}
}
}
// For each property in styles param, check if it is present in this._styles
// and remove it.
public removeStyles (styles: Array<string>): void {
for (const property of styles) {
if (this._styles.hasOwnProperty(property)) {
delete this._styles[property];
}
}
}
public set styles (styles: { [property: string]: string }) {
this.addStyles(styles);
}
public get styles (): { [property: string]: string } {
return this._styles;
}
}
让我真正兴奋和期待的是 ES6 中装饰器规范的标准化。 Typescript 通过在 tsconfig.json
中设置 experimentalDecorators
标志来允许此实验性功能。我希望 StylableClass
用作 class 装饰器 (@Stylable
) 以使代码更清晰,因此我创建了一个函数,该函数接受 class 并将其转换为装饰器:
export function makeDecorator (decorator: Function) {
return function (decorated: Function) {
const fieldCollector: { [key: string]: string } = {};
decorator.apply(fieldCollector);
Object.getOwnPropertyNames(fieldCollector).forEach((name) => {
decorated.prototype[name] = fieldCollector[name];
});
Object.getOwnPropertyNames(decorator.prototype).forEach((name) => {
decorated.prototype[name] = decorator.prototype[name];
});
};
}
并像这样使用它:
export const Stylable = () => makeDecorator(StylableClass);
问题
现在是单元测试的时候了。我创建了一个虚拟 class 来应用我的装饰器并为 addStyles()
方法编写了一个简单的测试。
@Stylable()
class StylableTest {
// Stylable
public addStyles!: (styles: {
[prop: string]: string;
}) => void;
public updateStyles!: (styles: {
[prop: string]: string;
}) => void;
public removeStyles!: (styles: string[]) => void;
public styles: { [property: string]: string } = {};
}
describe('Test Stylable mixin', () => {
it('should add styles', () => {
const styles1 = {
float: 'left',
color: '#000'
};
const styles2 = {
background: '#fff',
width: '100px'
};
// 1
const styles = new StylableTest();
expect(styles.styles).to.be.an('object').that.is.empty;
// 2
styles.addStyles(styles1);
expect(styles.styles).to.eql(styles1);
// 3
styles.addStyles(styles2);
expect(styles.styles).to.eql(Object.assign({}, styles1, styles2));
});
});
问题是第二个 expect 语句失败了。在执行 styles.addStyles(styles1);
之后,styles.styles
数组应该包含 styles1
对象时仍然是空的。当我调试我的代码时,我发现 push
方法中的 addStyles()
语句按预期执行,因此循环没有问题,但是方法执行结束后数组没有更新。您能否就我遗漏的内容提供提示或解决方案?我检查的第一件事是 makeDecorator
函数可能出了问题,但只要我可以执行这些方法,我就找不到其他线索来寻找。
StylableClass
mixin 声明了一个名为 styles
的 属性。但是 StylableTest
创建了一个字段名称 styles
并为其分配了一个没有人会使用的空对象。您需要将 属性 描述从装饰器转移到目标 class,并从 StylableTest
中的 styles
中删除 = {}
:
function makeDecorator(decorator) {
return function (decorated) {
var fieldCollector = {};
decorator.apply(fieldCollector);
Object.getOwnPropertyNames(fieldCollector).forEach(function (name) {
decorated.prototype[name] = fieldCollector[name];
});
Object.getOwnPropertyNames(decorator.prototype).forEach(function (name) {
var descriptor = Object.getOwnPropertyDescriptor(decorator.prototype, name);
if (descriptor) {
Object.defineProperty(decorated.prototype, name, descriptor);
}
else {
decorated.prototype[name] = decorator.prototype[name];
}
});
};
}
我可以建议 less error prone approach 在打字稿中混入。这种必须重新声明所有 mixin 成员的方法将在以后导致错误。至少避免使用类型查询重述字段的类型:
@Stylable()
class StylableTest {
// Stylable
public addStyles!: IStylable['addStyles']
public updateStyles!: IStylable['updateStyles']
public removeStyles!: IStylable['removeStyles']
public styles!: IStylable['styles']
}