vue-class-setup 编写 class 风格组合式API

 

前言

我司基于vue-class-component开发的项目有上百个,其中部署的 SSR 服务也接近100个,如此庞大体量的项目一开始的时候还幻想着看看是否要升级Vue3,结果调研一番下来,才发现vue-class-component对Vue3的支持,最后一个版本发布都过去两年了,迟迟还没有发布正式版本。目前基本上处于无人维护的状态,而且升级存在着大量的破坏性更新,对于未来是否还要继续使用Vue3现在还是持保留意见,但是不妨碍我们先把组件库做成Vue2和Vue3通用,于是就有了本文。

在过去的三年里,vue-class-component最大的问题是就是无法正确的校验组件的传参,事件类型,这给我带来了巨大的阴影,在经过一番调研后,惊喜的发现使用defineComponent定义的组件,在Vue2.7和3.x都可以正确的识别类型,所以先计划内部的组件库先做到同时支持Vue2和Vue3,如果后面还要继续采用Vue3就变得容易得多。

于是,回到了开头,调研了一番vue-class-component在Vue3的支持,目前最新的版本是8.0.0-rc.1,结果大失所望,目前基本上处于无人维护的状态,社区内又没有一个能满足我需求的,同时支持Vue2和Vue3的。

 

诞生想法

鉴于vue-class-component组件目前无法做到正确的组件类型检验,当我惊喜的发现组合式API写出来的代码可以被正确的识别类型时,诞生了一个使用 class 风格来编写组合式API的想法,于是花费一个月的实践,踩遍了所有的坑,终于诞生了vue-class-setup,一个使用 class 风格来编写代码的库,它gzip压缩后,1kb大小。

 

快速开始

npm install vue-class-setup
<script lang="ts">
import { defineComponent } from 'vue';
import { Setup, Context } from 'vue-class-setup';
// Setup 和 Context 必须一起工作
@Setup
class App extends Context {
  private _value = 0;
  public get text() {
      return String(this._value);
  }
  public set text(text: string) {
      this._value = Number(text);
  }
  public onClick() {
      this._value++;
  }
}
export default defineComponent({
  // 注入类实例的逻辑
  ...App.inject(),
});
</script>
<template>
  <div>
      <p>{{ text }}</p>
      <button @click="onClick()"></button>
  </div>
</template>

尝试多很多种方案,最终采用了上面的形式为最佳实践,它无法做到export default直接导出一个类,必须使用defineComponent 来包装一层,因为它只是一个组合类(API),并非是一个组件。

 

最佳实践

<script lang="ts">
import { defineComponent } from 'vue';
import { Setup, Define } from 'vue-class-setup';
// 传入组件的 Props 和 Emit,来让组合类获取正确的 `Props` 和 `Emit` 类型
@Setup
class App extends Define<Props, Emit> {
  // ✨ 你可以直接这里定义Props的默认值,不需要像 vue-property-decorator 那样使用一个 Prop 装饰器来定义
  public readonly dest = '--';
  // 自动转换成 Vue 的 'computed'
  public get text() {
      return String(this.value);
  }
  public click(evt: MouseEvent) {
      // 发射事件,可以正确的识别类型
      this.$emit('click', evt);
  }
}
/**
* 这里提供了另外一种在 setup 函数中使用的例子,默认推荐使用 `defineComponent`
* 如果有多个类实例,也可以在 setup 中实例化类
* <script lang="ts" setup>
*      const app = new App();
* <\/script>
* <template>
*      <div>{{ app.text }}</div>
* </template>
*/
export default defineComponent({
  ...App.inject(),
});
</script>
<script lang="ts" setup>
// 如果在 setup 中定义类型,需要导出一下
export interface Props {
  value: number;
  dest?: string;
}
export interface Emit {
  (event: 'click', evt: MouseEvent): void;
}
// 这里不再需要使用变量来接收,可以利用 Vue 的编译宏来为组件生成正确的 Props 和 Emit
// ❌ const props = defineProps<Props>();
// ❌ const emit = defineEmits<Emit>();
defineProps<Props>(); //  ✅
defineEmits<Emit>(); //  ✅
// 这种默认值的定义,也不再推荐,而是直接在类上声明
// ❌ withDefaults(defineProps<Props>(), { dest: '--' });
// ✅ @Setup
// ✅ class App extends Define<Props, Emit> {
// ✅     public readonly dest = '--'
// ✅ }
// Setup 装饰器,会在类实例化时,自动 使用 reactive 包装类,
// 如果你在 setup 手动实例化,则不需要再执行一次 reactive 
// const app = reactive(new App()); // ❌
// const app = new App();           // ✅
</script>
<template>
  <button class="btn" @click="click($event)">
      <span class="text">{{ text }}</span>
      <span class="props-dest">{{ dest }}</span>
      <span class="props-value">{{ $props.value }}</span>
  </button>
</template>

 

多个类实例

在一些复杂的业务时,有时需要多个实例

<script lang="ts">
import { onBeforeMount, onMounted } from 'vue';
import { Setup, Context, PassOnTo } from 'vue-class-setup';
@Setup
class Base extends Context {
  public value = 0;
  public get text() {
      return String(this.value);
  }
  @PassOnTo(onBeforeMount)
  public init() {
      this.value++;
  }
}
@Setup
class Left extends Base {
  public left = 0;
  public get text() {
      return String(`value:${this.value}`);
  }
  public init() {
      super.init();
      this.value++;
  }
  @PassOnTo(onMounted)
  public initLeft() {
      this.left++;
  }
}
@Setup
class Right extends Base {
  public right = 0;
  public init() {
      super.init();
      this.value++;
  }
  @PassOnTo(onMounted)
  public initLeft() {
      this.right++;
  }
}
</script>
<script setup lang="ts">
const left = new Left();
const right = new Right();
</script>
<template>
  <p class="left">{{ left.text }}</p>
  <p class="right">{{ right.text }}</p>
</template>

 

PassOnTo

在类实例准备就绪后,PassOnTo 装饰器,会将对应的函数,传递给回调,这样我们就可以顺利的和 onMounted 等钩子一起配合使用了

import { onMounted } from 'vue';
@Setup
class App extends Define {
  @PassOnTo(onMounted)
  public onMounted() {}
}

 

Watch

在使用 vue-property-decorator 的 Watch 装饰器时,他会接收一个字符串类型,它不能正确的识别类实例是否存在这个字段,但是现在 vue-class-setup 能检查你的类型是否正确,如果传入一个类实例不存在的字段,类型将会报错

<script lang="ts">
import { Setup, Watch, Context } from 'vue-class-setup';
@Setup
class App extends Context {
  public value = 0;
  public immediateValue = 0;
  public onClick() {
      this.value++;
  }
  @Watch('value')
  public watchValue(value: number, oldValue: number) {
      if (value > 100) {
          this.value = 100;
      }
  }
  @Watch('value', { immediate: true })
  public watchImmediateValue(value: number, oldValue: number | undefined) {
      if (typeof oldValue === 'undefined') {
          this.immediateValue = 10;
      } else {
          this.immediateValue++;
      }
  }
}
</script>
<script setup lang="ts">
const app = new App();
</script>
<template>
  <p class="value">{{ app.value }}</p>
  <p class="immediate-value">{{ app.immediateValue }}</p>
  <button @click="app.onClick()">Add</button>
</template>

 

defineExpose

在一些场景,我们希望可以暴露组件的一些方法和属性,那么就需要使用 defineExpose 编译宏来定义导出了,所以提供了一个.use的类静态方法帮你获取当前注入的类实例

<script lang="ts">
import { defineComponent } from 'vue';
import { Setup, Context } from 'vue-class-setup';
@Setup
class App extends Context {
  private _value = 0;
  public get text() {
      return String(this._value);
  }
  public set text(text: string) {
      this._value = Number(text);
  }
  public addValue() {
      this._value++;
  }
}
export default defineComponent({
  ...App.inject(),
});
</script>
<script lang="ts" setup>
const app = App.use();
defineExpose({
  addValue: app.addValue,
});
</script>
<template>
  <div>
      <p class="text">{{ text }}</p>
      <p class="text-eq">{{ app.text === text }}</p>
      <button @click="addValue"></button>
  </div>
</template>

 

为什么使用 class ?

其实不太想讨论这个问题,喜欢的自然会喜欢,不喜欢的自然会不喜欢,世上本无路,走的人多了,就有了路。

 

最后

不管是 选项 API 还是 组合式API,代码都是人写出来的,别人都说 Vue 无法胜任大型项目,但是在我司的实践中经受住了实践,基本上没有产生那种数千行的组件代码。

如果喜欢使用 class 风格来编写代码的,不妨来关注一下

  • vue-class-setup

如果你的业务复杂,需要使用 SSR 和微服务架构,不妨也关注一下

  • vue-genesis

以上就是vue-class-setup 编写 class 风格组合式API的详细内容,更多关于vue-class-setup 编写组合式API的资料请关注编程宝库其它相关文章!

 子组件传父组件 $emit更新属性$emit(update: prop, “newPropVulue”) => $emit(update: 属性名, “新的属性值”)例如修改 ...