变成我们需要使用装饰器将 TypeScript class 转换为有效的 Vue 组件?

Into what we need to convert the TypeScript class using decorators to get the valid Vue component?

如果我问“vue-class-component 是如何工作的”,很可能这个问题被标记为过于宽泛。顺便说一句,我查看了它的源代码,但不明白它是如何工作的,所以我需要从简单的东西开始。

这是来自 Vue documentation 的简单示例:

export default {
  props: ['foo'],
  created() {
    console.log(this.foo)
  }
}

从 ECMAScript(甚至 OOP)的角度来看,下面的 class 不等同于上面的对象。

export default class Component {

  private foo!: string;

  protected created(): void {
    console.log(this.foo)
  }
}

所以,我想问题陈述是使用如下装饰器

@MagicDecorator
class Component {

  @VueProperty({ type: String })
  protected foo!: string;

  @VueLifecycleHook
  protected created(): void {
   console.log(this.foo)
  }
}

将其转换为第一个列表。这个问题陈述对吗?

请注意,我没有完全照原样做的目标 vue-class-component - 欢迎改进。例如,我将向生命周期挂钩、数据和计算属性添加装饰器,这与 vue-class-component.

不同

是的,你是对的。装饰师使出了所有的魔法。它与 TypeScript 没有直接关系。仅使用 JavaScript 和 babel 的装饰器插件即可完成。我觉得vue-class-components的源码已经说明了一切,还是自己搭建一个最小版本吧。为了简单起见,我只使用 JavaScript。

我们的目标是创建一个可以将 class 转换为 vue 组件对象的装饰器,例如:

class MyComponent {
  data() {
    return {
      count: 0,
    };
  }
  plus() {
    this.count++;
  }
}
// will be converted to something like
const MyComponent = {
  data() {
    return {
      count: 0,
    };
  },
  methods: {
    plus() {
      this.count++;
    }
  }
}

其实很简单。我们创建一个新对象,并将class的所有方法复制到该对象中。让我们先创建装饰器函数:

function MagicDecorator(ComponentClass) {
  const options = {};
  return options;
}

options就是我们转换后的结果。现在,我们要遍历 class,找出它有什么属性和方法。

function MagicDecorator(ComponentClass) {
  const options = {};
  Object.getOwnPropertyNames(ComponentClass.prototype).forEach((key) => {
    console.log(key); // data, plus
  });
  return options;
}

请注意,Object.keys(ComponentClass.prototype) 将不起作用。因为这些是 non-enumerable 属性(由 Object.defineProperty() 定义)

现在,对于mountedcreateddata等built-in钩子方法,我们直接复制即可。您可以在 Vue 源代码中找到 hook 方法列表。

const hooks = [
  'data',
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeDestroy',
  'destroyed',
  'beforeUpdate',
  'updated',
  'activated',
  'deactivated',
  'render'
];

function MagicDecorator(ComponentClass) {
  const options = {};
  Object.getOwnPropertyNames(ComponentClass.prototype).forEach((key) => {
    if (hooks.includes(key)) {
      options[key] = ComponentClass.prototype[key];
    }
  });
  return options;
}

是的,就是这么简单,只需将其复制到我们的options

现在,对于自定义方法,我们必须将其放在 methods 对象中。

function MagicDecorator(ComponentClass) {
  const options = {
    methods: {},
  };
  Object.getOwnPropertyNames(ComponentClass.prototype).forEach((key) => {
    if (hooks.includes(key)) {
      options[key] = ComponentClass.prototype[key];
      return
    }
    if (typeof ComponentClass.prototype[key] === 'function') {
      options.methods[key] = ComponentClass.prototype[key];
    }
  });
  return options;
}

实际上现在它已经可以工作了,可以处理许多简单的组件!和上面的计数器组件一样,我们的装饰器现在完全支持它。

但是我们知道 Vue 有计算属性。让我们也支持这个功能吧。

通过 getters 和 setters 支持计算属性。现在有点棘手,因为你会注意到我们可以通过

直接访问它们
ComponentClass.prototype[key]; // This will trigger the getter

因为当您以这种方式访问​​它时,您实际上是在调用 getter。幸运的是,我们可以使用 Object.getOwnPropertyDescriptor() 来获得实际的 getter 和 setter 函数。然后我们需要做的就是将其放入 computed 字段。

const options = {
  methods: {},
  computed: {},
};

// omit...

const descriptor = Object.getOwnPropertyDescriptor(
  ComponentClass.prototype,
  key
);
if (descriptor.get || descriptor.set) {
  options.computed[key] = {
    get: descriptor.get,
    set: descriptor.set
  };
}

在 vue-class-components 的源代码中,它们也通过使用描述符来处理 methods

if (typeof descriptor.value === 'function') {
  options.methods[key] = descriptor.value;
  return;
}

最后,我们将不支持构造函数。我们只是将它添加到循环的第一个并忽略它:

if (key === 'constructor') {
  return;
}

现在,我们有了一个完整的工作示例。在此处查看实际效果:https://codesandbox.io/s/Whosebug-vue-class-component-uhh2jg?file=/src/MagicDecorator.js

注意 1:我们的最小示例不支持使用简单 class 属性:

的数据
class MyComponent {
  count = 0 // We don't support this

  // Only this is supported
  data() {
    return { count: 0 }
  }
}

如果我们想支持 class 属性,我们必须自己将其转换为响应式 属性。

注2:Babel支持两种版本的装饰器。为了与 vue-class-component 的源代码保持一致,我使用的是遗留代码。所以你必须在 @babel/plugin-proposal-decorators 插件中指定 {legacy: true} 选项。