写个兼容Vue3和2的组件

2,799 阅读2分钟

起因

有个 Vue 项目用到 ECharts,组内几个前端菜鸟集成得乱七八糟的,于是考虑使用 ECharts 的包装组件。在研究了几个热门的封装组件后,发现存在依赖内置、过度封装、使用繁琐、不支持 Vue3 等问题,于是乎自己动手造轮子(>>成品传送门<<)。

Vue 3 的变化

Vue 3 并非完全颠覆性的版本,它向前兼容 Vue 2 的大部分写法。本文只对变更部分稍作介绍,详细内容请阅读官方文档

    1. 新增组合 API(Composition API),利于拆分代码、聚合逻辑关注点;
    1. 全局 API 改由 ES 模块进行命名导出,有助于 Tree shaking 消除死代码;
import { createApp, nextTick } from 'vue';
    1. render 函数不再接收 h 函数,改由全局 API 获取;
import { h } from 'vue';
    1. VNode props 改为扁平结构;
// Vue 2
export default {
    render: () => h('div', {
        attrs: { id: 'wrapper' },
        domProps: { innerText: 'Hello World' },
        on: { click() {} },
    }),
}
// Vue 3
export default {
    render: () => h('div', {
        id: 'wrapper',
        innerText: 'Hello World',
        onClick() {},
    }),
}
    1. 生命周期选项destroyedbeforeDestroy分别被重命名为unmountedbeforeUnmount

组件主体设计

我们通常采用直观的单文件组件形式进行设计,如下所示:

<template>
    <div ref="chart" v-bind="$attrs" />
</template>

<script>
    import echarts from 'echarts';
    export default {
        mounted() {
            const inst = echarts.init(this.$refs.chart);
            inst.setOption({ /* ... */ });
        },
    }
</script>

然而,单文件组件无法在运行时直接使用,需经由编译器翻译成 Vue 可识别的代码。由于 Vue 3 和 Vue 2 的差异,各自的编译器编译后的代码无法互相兼容,因此改用渲染函数替代 template,也便于进行兼容处理。

// 变量 isVue3、h 见下文“依赖内置问题”章节
export default {
    render: isVue3 ? getVue3Render(h) : vue2Render,
    mounted() {
        const inst = echarts.init(this.$el);
        inst.setOption({ /* ... */ });
    },
}

function vue2Render(h) {
    return h('div', {
        attrs: this.$attrs,
    });
}

function getVue3Render(h) {
    return function () {
        return h('div', {
            ...this.$attrs,
        });
    };
}

生命周期的处理

由于destroyedbeforeDestroy分别被unmountedbeforeUnmount替代,因此直接判断 Vue 版本,更改狗子名称即可。

const hooks = {
    mounted() {},
    beforeUnmount() {},
};

if (!isVue3) {
    hooks.beforeDestroy = hooks.beforeUnmount;
    delete hooks.beforeUnmount;
}

依赖内置问题

vue-echarts为例,源码中通过 import 导入依赖,并在 package.json 的peerDependencies中声明依赖关系。

import echarts from 'echarts/lib/echarts'
import debounce from 'lodash/debounce'
import { addListener, removeListener } from 'resize-detector'

在某些环境中,默认情况下node_modules中的文件会被排除在编译或打包范围之外,所以vue-echarts包中的依赖不能正确导入,需要额外配置使其能够正常工作。如下:

// "vue.config.js"
module.exports = {
    transpileDependencies: [
        'vue-echarts',
        'resize-detector',
    ],
}

当然,需要额外配置只是个小问题,比较麻烦的是ECharts 有完全版、常用版、精简版,还支持按需引入,而源码中已经把引入的版本定死了。为此,我采用依赖外置的方式,并创建函数createComponent用于生成组件的定义对象,将外置的依赖作为函数的参数传入,保持函数的纯粹性。

/**
 * Create a component
 * @param {object} options 
 * @param {echarts} options.echarts
 * @param {function} [options.h] `createElement`, required for Vue 3
 * @returns {object} definition
 */
export function createComponent({ echarts, h }) {
    const isVue3 = typeof h === 'function';
    const hooks = { /* ... */ };
    if (!isVue3) {
        hooks.beforeDestroy = hooks.beforeUnmount;
        delete hooks.beforeUnmount;
    }
    return {
        render: isVue3 ? getVue3Render(h) : vue2Render,
        ...hooks,
    };
}

测试

由于涉及同一个包的不同版本,为了能在同个项目完成测试,可通过 npm 别名来实现。(npm 版本不得小于 6.9)

# npm i <alias>@npm:<packageName>@<version>

npm i vue2@npm:vue@2
npm i vue3@npm:vue@3

然后,设置 vue 的别名为 vue2 或 vue3

// Webpack
module.exports = {
    resolve: {
        alias: {
            vue: 'vue2',
        },
    },
}
// Vite
export default {
    alias: {
        vue: 'vue3',
    },
}

注意:如果编译失败,检查node_modules下是否存在vue目录。当存在vue目录时,别名不起作用,移除即可。

最后

轮子已发布 echarts-for-vue,欢迎评论、留言。

多多点赞,会变好看 👍