之前利用业余时间开发了一个 Vue 插件,那会市场还是 Vue2 的时代。如今,Vue3 已然成为了必然趋势,为了让项目有更长的生命周期,有必要升级一下,让这个库也支持 Vue3。
效仿 Vue,建两个仓库,一个适配 v2,一个适配 v3,取名 xxx
和 xxx-next
。
与方案一类似,在仓库中建两个分支 v2 和 v3,分别支持 Vue 的两个版本。
优势与劣势与方案一相同,唯一不同是只需要一个仓库,但是维护成本同样很大。
以上两种方案都需要维护两套代码,那么有没有一种解决方案是只用一套代码就能搞定的呢
vue-demi
?vue-demi
是一个让你可以开发同时支持 Vue2 和 3 的通用的 Vue 库的开发工具,而无需担心用户安装的版本。官方仓库[1],是由 Vue 团队核心成员 antfu 开发的。vueuse
, vue-charts
等包都使用了它。
有兴趣的可以去看看作者对 vue-demi 的介绍[2]
任何与 Vue 相关的 API,都不再从原先的 vue
引入,而是从 vue-demi
引入。
import { ref, reactive, defineComponent } from 'vue-demi'
其余代码就像平常开发 Vue 时一样的去 coding 和发布就行了!
vue-demi
原理往往使用起来越简单的代码,隐藏在其之下的原理就越值得探究。那么 vue-demi 究竟有什么黑科技呢?
vue-demi
利用了 NPM 的 postinstall
钩子。当用户安装所有包后,脚本将开始检查已安装的 Vue 版本,并根据 Vue 版本返回对应的代码。在使用 Vue2 时,如果没有安装 @vue/composition-api
,它也会自动安装。
以下摘取了部分核心代码:
const Vue = loadModule('vue'); // 加载 vue
function switchVersion(version, vue) {
// 将提前写好的文件 index 文件 copy 进去
copy('index.cjs', version, vue);
copy('index.mjs', version, vue);
copy('index.d.ts', version, vue);
if (version === 2) {
updateVue2API(); // 在 Vue2 时,安装 @vue/composition-api
}
}
// 判断版本号,将对应的文件写入 vue-demi 的导出文件
if (Vue.version.startsWith('2.')) {
switchVersion(2);
} else if (Vue.version.startsWith('3.')) {
switchVersion(3);
}
回到方案上来看:
@vue/composition-api
,会稍微提升代码体积。为了让项目能低成本,快速地支持 Vue3(私心也想体验一些新的轮子)。
最终我选择方案三:使用 vue-demi。
vue-demi
npm i vue-demi
# or
yarn add vue-demi
将 vue
和 @vue/composition-api
添加到 package.json
的 peerDependencies
中。
{
"dependencies": {
"vue-demi": "latest"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^2.0.0 || >=3.0.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
},
"devDependencies": {
"vue": "^3.0.0"
},
}
改造前:
const MyPlugin = {
/**
* install function
* @param {Vue} Vue
* @param {Object} options
*/
install (Vue, options = {}) {
... // 插件的默认参数处理
// 全局注册组件
Vue.component('my-component', MyComponent);
},
};
export default MyPlugin;
由于 Vue3 中插件的 install
方法传入的不再是 Vue 构造函数
,而是 app 实例
,这里只需要调整形参名即可:Vue
-> app
。
改造后:
const MyPlugin = {
/**
* install function
* @param {App} app
* @param {Object} options
*/
install (app, options = {}) {
... // 插件的默认参数处理
// 全局注册组件
app.component('my-component', MyComponent);
},
};
export default MyPlugin;
为了支持 Vue3,我们需要尽可能的使用 Vue3 的新语法。同时,也为了让代码改动尽可能小,我这次没有使用 setup API。
改造前:
代码是 Vue2 组件定义语法,定义一个组件对象并向外默认导出。
export default {
name: ...
props: ...
watch: ...
};
在 Vue3 中,我们使用 defineComponent
这个全新的 API,用于 TypeScript
的类型推导,包裹该组件对象。
不一样的是,这里的 defineComponent
需要从 vue-demi
引入。
改造后:
import { defineComponent } from 'vue-demi'; // 需要从 `vue-demi` 引入
export default defineComponent({
name: ...
props: ...
watch: ...
});
改造前:
createElement
的方法,通常我们用作 h
。default
)插槽 VNode,我们可以使用 this.$slots.default
。render(h) {
...
const slot = this.$slots.default; // 默认插槽
return h('div', null, slot); // 将传入的默认插槽内容使用 div 包裹
}
Vue3 中 render 方法不再提供 h
方法,需要自行从 vue
引入。同样,这里也从 vue-demi
引入。
获取默认插槽需要将 this.slots.default 作为方法调用 this.slots.default()。
但 this.$slots.default
无法从 vue-demi
引入,又与当前运行时的 Vue 版本有关,该怎么办呢?
vue-demi
为我们提供了两个额外的 API,isVue2
和 isVue3
,用于判断当前的环境。
改造后:
import { h, isVue2 } from 'vue-demi'; // 需要从 `vue-demi` 引入
render(h2) {
...
// vue2
if (isVue2) {
const slot = this.$slots.default; // 默认插槽
return h2('div', null, slot);
}
// vue3
const slot = this.$slots.default(); // 默认插槽
return h('div', null, slot);
}
改造前:
我们可以很容易的使用 Vue2 中的 emit 和 on 来实现事件总线(EventBus)。在我的这个库中,子组件需要派发事件到指定的祖先组件,我借鉴了 element-ui 利用 `和on` 的实现[3]:
<Ancestor>
在生命周期中监听事件created() {
this.$on('event', handler)
}
// 派发事件到指定祖先组件
export default defineComponent({
...
methods: {
$_dispatchComponent(componentName, event, args) {
let parent = this.$parent || this.$root;
let name = parent.$options.name;
// 通过循环不断向上查找 name 一致的组件
while (parent && (!name || name !== componentName)) {
parent = parent.$parent;
if (parent) {
name = parent.$options.name;
}
}
if (parent) {
parent.$emit.call(parent, event, args); // 找到后,派发事件
}
},
},
},
Vue3 中,由于移除了 $on
,实现事件总线已经没办法使用 Vue 自身的 API 了。
我们需要借助第三方库来完成,例如 mitt[4] 或 tiny-emitter[5]。这里我选择了 mitt
,API 够用,也比较轻量。
改造后:
emitter.on
代替 $on
:import mitt from 'mitt';
export default defineComponent({
...
created() {
this.emitter = mitt();
this.emitter.on(event, handler); // 监听事件
},
beforeUnmount() {
this.emitter.all.clear(); // 解绑事件
}
})
parent.$emit
改成 parent.emitter.emit
。parent.emitter.emit(event, args);
vue-demi
来开发同时支持 Vue2 和 vue3 的第三方包,开发和迁移成本小。vue-demi
的开发体验与平时开发 Vue 一致,心智负担小。vue-demi
为我们提供了额外的 API,isVue2
和 isVue3
,用于判断当前的环境。mitt
或 tiny-emitter
。如果本文对你有帮助,点赞👍支持下我吧,你的「赞」是我创作的动力。
关于我,目前是字节跳动一线开发,工作四年半,工作中使用 React,业余时间开发喜欢 Vue。
参考资料
[1]
官方仓库: https://github.com/vueuse/vue-demi
[2]
作者对 vue-demi 的介绍: https://antfu.me/posts/make-libraries-working-with-vue-2-and-3#-introducing-vue-demi
[3]
element-ui 利用 emit 和 on 的实现: https://github.com/ElemeFE/element/blob/dev/src/mixins/emitter.js
[4]
mitt: https://github.com/developit/mitt
[5]
tiny-emitter: https://github.com/scottcorgan/tiny-emitter
[6]
github 仓库: https://github.com/Leecason/vue-rough-notation
[7]
在线地址: https://leecason.github.io/vue-rough-notation/