使用 vue-class-setup 编写 class 风格来的组合式 API,支持 Vue2 和 Vue3 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
爱意满满的作品展示区。
1340641314
V2EX    分享创造

使用 vue-class-setup 编写 class 风格来的组合式 API,支持 Vue2 和 Vue3

  •  
  •   1340641314
    lzxb 2022-09-23 00:57:46 +08:00 2183 次点击
    这是一个创建于 1116 天前的主题,其中的信息可能已经有所发展或是发生改变。

    前言

    我司基于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-decoratorWatch 装饰器时,他会接收一个字符串类型,它不能正确的识别类实例是否存在这个字段,但是现在 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="txt">{{ text }}</p> <p class="text-eq">{{ app.text === text }}</p> <button @click="addValue"></button> </div> </template> 

    为什么使用 class ?

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

    最后

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

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

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

    9 条回复    2022-09-23 19:48:43 +08:00
    gouflv
        1
    gouflv  
       2022-09-23 08:22:36 +08:00 via iPhone   1
    class component 是 vue 最接近 ng 的时候,以后不会再有了
    suzic
        2
    suzic  
       2022-09-23 08:26:57 +08:00 via Android
    electron qq 团队的开发者?
    1340641314
        3
    1340641314  
    OP
       2022-09-23 09:19:23 +08:00
    @suzic 不是的。。。
    1340641314
        4
    1340641314  
    OP
       2022-09-23 09:20:13 +08:00
    @gouflv 官方不会再推进使用 class 组件了
    shakaraka
        5
    shakaraka  
    PRO
       2022-09-23 09:59:10 +08:00
    现在的 vue 和 react 走一条路了,还是我 ng 好
    1340641314
        6
    1340641314  
    OP
       2022-09-23 10:11:38 +08:00
    @wunonglin 目前没办法,公司上百个项目,可不是闹着玩。
    可以了解一下这个,目前处于 beta 阶段
    https://github.com/BuilderIO/qwik

    后面可能会往这个技术栈上转,能解决我们目前很多在 Vue SSR 微服务领域遇到的很多问题
    yunyuyuan
        7
    yunyuyuan  
       2022-09-23 18:06:22 +08:00
    有趣。ng 用 class ,优点在于 @Input ,constructor()依赖注入,这种附带功能。vue3 的 setup 学习了 react 的 hook ,优点在于代码组织的灵活性。用 class 写 setup ,是不是两者的优点都丢了呢
    1340641314
        8
    1340641314  
    OP
       2022-09-23 19:46:57 +08:00
    @yunyuyuan 其实它的本质还是 setup ,只不过是使用 class 的形式来编写出来而已。其实还有一点是文章没有讲的,写多了 ref, reactive , computed 和 withDefaults ,就觉得真的很烦,使用 class 的代码组织形式,可以让我忘记一直需要调用 Vue 的各种 API
    1340641314
        9
    1340641314  
    OP
       2022-09-23 19:48:43 +08:00
    还有一点的就是继续延续自身团队的编程风格,和 vue-class-component 可以比较无缝衔接
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     4912 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 24ms UTC 09:51 PVG 17:51 LAX 02:51 JFK 05:51
    Do have faith in what you're doing.
    ubao snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86