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']
}