V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
爱意满满的作品展示区。
1340641314
V2EX  ›  分享创造

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

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

    前言

    我司基于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="text">{{ 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 组件了
    wunonglin
        5
    wunonglin  
       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   ·   实用小工具   ·   2962 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 00:35 · PVG 08:35 · LAX 16:35 · JFK 19:35
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.