如何实现同一个项目里面支持VUE2与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 库的开发工具,而无需担心用户安装的版本。官方仓库,是由 Vue 团队核心成员 antfu 开发的。vueuse, vue-charts 等包都使用了它。

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

使用方法

任何与 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);
}

回到方案上来看:

优势:

  1. 开发者没有心智负担,和平时开发 Vue 项目的开发体验一致。
  2. 只需要维护一套代码,代码库也不会出现两个大版本同时维护的情况,开发成本低。

劣势:

  1. 使用 Vue2 的开发者需要额外安装 @vue/composition-api,会稍微提升代码体积。

结论

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

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

迁移过程

安装 vue-demi

npm i vue-demi
# or
yarn add vue-demi

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

{
  "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 插件

改造前:

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;

Vue 单文件组件

为了支持 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: ...
  
});

渲染函数 render

改造前:

  1. Vue2 中渲染函数 render 方法会提供一个 createElement 的方法,通常我们用作 h
  2. 要在 render 方法中获取当前的默认(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,isVue2isVue3,用于判断当前的环境。

改造后:

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 利用 `emit` 和 `on` 的实现

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

我们需要借助第三方库来完成,例如 mitttiny-emitter。这里我选择了 mitt,API 够用,也比较轻量。

改造后:

  1. 祖先组件使用 emitter.on 代替 $on
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
parent.emitter.emit(event, args);

项目源码

小结

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

链接:https://www.zhihu.com/question/475451857/answer/2377600057

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Vue实现两个不同table的层级关联可以通过以下步骤实现: 1. 在Vue的data中定义两个table对应的数据源,分别为table1Data和table2Data。 2. 在Vue的methods中定义一个方法,用于处理层级关联逻辑。例如,可以定义一个方法名为handleLevelRelation的方法。 3. 在handleLevelRelation方法中,可以通过遍历table1Data的每个数据项,然后根据当前数据项的某个字段值,在table2Data中筛选出对应的子级数据项。可以使用Array的filter方法来实现这个筛选功能。 4. 在Vue的template中,使用v-for指令来循环渲染table1Data的每个数据项,然后在循环内部再使用嵌套的v-for指令来渲染对应的子级数据项。 5. 在嵌套的v-for指令中,使用v-if指令来判断当前数据项是否符合关联条件,如果符合则渲染对应的子级数据项。 以下是一个示例代码: ```html <template> <div> <table> <tr v-for="item1 in table1Data" :key="item1.id"> <td>{{ item1.name }}</td> <td> <table> <tr v-for="item2 in getChildren(item1.id)" :key="item2.id"> <td>{{ item2.name }}</td> </tr> </table> </td> </tr> </table> </div> </template> <script> export default { data() { return { table1Data: [ { id: 1, name: 'A' }, { id: 2, name: 'B' }, // ... ], table2Data: [ { id: 1, name: 'A-1', parentId: 1 }, { id: 2, name: 'A-2', parentId: 1 }, // ... { id: 11, name: 'B-1', parentId: 2 }, { id: 12, name: 'B-2', parentId: 2 }, // ... ] }; }, methods: { getChildren(parentId) { return this.table2Data.filter(item => item.parentId === parentId); } } }; </script> ``` 以上是一个简单的示例,使用Vue的v-for指令和v-if指令实现两个不同table的层级关联。在实际开发中,可以根据具体需求进行适当的修改和扩展。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值