前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >将 Vue 插件升级到同时支持 Vue2 和 3 的实践小结

将 Vue 插件升级到同时支持 Vue2 和 3 的实践小结

作者头像
Leecason
发布2022-07-13 14:20:45
1.1K0
发布2022-07-13 14:20:45
举报
文章被收录于专栏:小李的前端小屋

背景

之前利用业余时间开发了一个 Vue 插件,那会市场还是 Vue2 的时代。如今,Vue3 已然成为了必然趋势,为了让项目有更长的生命周期,有必要升级一下,让这个库也支持 Vue3。

方案

方案一:两个仓库

效仿 Vue,建两个仓库,一个适配 v2,一个适配 v3,取名 xxxxxx-next

优势:
  1. 有大量的社区实践,能直接从仓库名区分版本。
劣势:
  1. 仓库存在两个大版本号同时维护的场景,比如 v2.x 支持 Vue2,v3.x 支持 Vue3。
  2. 需要同时维护两套代码,此外,其中仓库工程化部分相同,存在大量重复代码。
  3. 如果之后要支持新特性或调整构建相关改动,需要同时处理两边代码,成本较大。

方案二:两个分支

与方案一类似,在仓库中建两个分支 v2 和 v3,分别支持 Vue 的两个版本。

优势与劣势与方案一相同,唯一不同是只需要一个仓库,但是维护成本同样很大。

以上两种方案都需要维护两套代码,那么有没有一种解决方案是只用一套代码就能搞定的呢

方案三:使用 vue-demi

什么是 vue-demi

vue-demi 是一个让你可以开发同时支持 Vue2 和 3 的通用的 Vue 库的开发工具,而无需担心用户安装的版本。官方仓库[1],是由 Vue 团队核心成员 antfu 开发的。vueuse, vue-charts 等包都使用了它。

有兴趣的可以去看看作者对 vue-demi 的介绍[2]

使用方法

任何与 Vue 相关的 API,都不再从原先的 vue 引入,而是从 vue-demi 引入。

代码语言:javascript
复制
import { ref, reactive, defineComponent } from 'vue-demi'

其余代码就像平常开发 Vue 时一样的去 coding 和发布就行了!

vue-demi 原理

往往使用起来越简单的代码,隐藏在其之下的原理就越值得探究。那么 vue-demi 究竟有什么黑科技呢?

vue-demi 利用了 NPM 的 postinstall 钩子。当用户安装所有包后,脚本将开始检查已安装的 Vue 版本,并根据 Vue 版本返回对应的代码。在使用 Vue2 时,如果没有安装 @vue/composition-api,它也会自动安装。

以下摘取了部分核心代码:

代码语言:javascript
复制
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);
}

回到方案上来看:

优势:
  1. 开发者没有心智负担,和平时开发 Vue 项目的开发体验一致。
  2. 只需要维护一套代码,代码库也不会出现两个大版本同时维护的情况,开发成本低。
劣势:
  1. 使用 Vue2 的开发者需要额外安装 @vue/composition-api,会稍微提升代码体积。

结论

为了让项目能低成本,快速地支持 Vue3(私心也想体验一些新的轮子)。

最终我选择方案三:使用 vue-demi

迁移过程

安装 vue-demi

代码语言:javascript
复制
npm i vue-demi
# or
yarn add vue-demi

vue@vue/composition-api 添加到 package.jsonpeerDependencies 中。

代码语言:javascript
复制
{
  "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"
  },
}

代码改造

Vue 插件

改造前:

代码语言:javascript
复制
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

改造后:

代码语言:javascript
复制
const MyPlugin = {
  /**
   * install function
   * @param {App} app
   * @param {Object} options
   */
  install (app, options = {}) {
    ... // 插件的默认参数处理
    
    // 全局注册组件
    app.component('my-component', MyComponent);
  },
};

export default MyPlugin;
Vue 单文件组件

为了支持 Vue3,我们需要尽可能的使用 Vue3 的新语法。同时,也为了让代码改动尽可能小,我这次没有使用 setup API。

组件定义

改造前:

代码是 Vue2 组件定义语法,定义一个组件对象并向外默认导出。

代码语言:javascript
复制
export default {
  name: ...
  
  props: ...
  
  watch: ...
  
};

在 Vue3 中,我们使用 defineComponent 这个全新的 API,用于 TypeScript 的类型推导,包裹该组件对象。

不一样的是,这里的 defineComponent 需要从 vue-demi 引入。

改造后:

代码语言:javascript
复制
import { defineComponent } from 'vue-demi'; // 需要从 `vue-demi` 引入

export default defineComponent({
  name: ...
  
  props: ...
  
  watch: ...
  
});
渲染函数 render

改造前:

  1. Vue2 中渲染函数 render 方法会提供一个 createElement 的方法,通常我们用作 h
  2. 要在 render 方法中获取当前的默认(default)插槽 VNode,我们可以使用 this.$slots.default
代码语言:javascript
复制
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,isVue2isVue3,用于判断当前的环境。

改造后:

代码语言:javascript
复制
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]:

  1. 祖先组件<Ancestor> 在生命周期中监听事件
代码语言:javascript
复制
created() {
  this.$on('event', handler)
}
  1. 子组件不断通过 parent 找到指定的祖先组件,找到后利用 parent.emit.call(parent, event, args) 向祖先元素派发事件。
代码语言:javascript
复制
// 派发事件到指定祖先组件
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 够用,也比较轻量。

改造后:

  1. 祖先组件使用 emitter.on 代替 $on
代码语言:javascript
复制
import mitt from 'mitt';

export default defineComponent({
  ...
  
  created() {
    this.emitter = mitt();

    this.emitter.on(event, handler); // 监听事件
  },
  
  beforeUnmount() {
    this.emitter.all.clear(); // 解绑事件
  }
})
  1. 子组件派发事件的方法从 parent.$emit 改成 parent.emitter.emit
代码语言:javascript
复制
parent.emitter.emit(event, args);

项目源码

  • github 仓库[6]
  • 在线地址[7]

小结

  1. 我们可以利用 vue-demi 来开发同时支持 Vue2 和 vue3 的第三方包,开发和迁移成本小。
  2. 使用 vue-demi 的开发体验与平时开发 Vue 一致,心智负担小。
  3. vue-demi 为我们提供了额外的 API,isVue2isVue3,用于判断当前的环境。
  4. 在 Vue3 中实现事件总线,需要借助第三方包,如 mitttiny-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/

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-03-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 小李的前端小屋 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景
  • 方案
    • 方案一:两个仓库
      • 方案二:两个分支
        • 方案三:使用 vue-demi
          • 结论
          • 迁移过程
            • 安装 vue-demi
              • 代码改造
              • 项目源码
              • 小结
              • ❤️支持
              相关产品与服务
              事件总线
              腾讯云事件总线(EventBridge)是一款安全,稳定,高效的云上事件连接器,作为流数据和事件的自动收集、处理、分发管道,通过可视化的配置,实现事件源(例如:Kafka,审计,数据库等)和目标对象(例如:CLS,SCF等)的快速连接,当前 EventBridge 已接入 100+ 云上服务,助力分布式事件驱动架构的快速构建。
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档