likes
comments
collection
share

vue-demi你真的会用吗

作者站长头像
站长
· 阅读数 19

背景介绍

接到了一个需求,在多个系统上添加同一个功能,这多个系统中包含了 Vue2Vue3 ,为了多系统之间的复用,决定开发一个 Vue2Vue3 都可以集成的插件

vue-demi 插件天生就是为了帮助完成这件事情的。

github(vue-demi)

在开发中遇到了很多问题,看了很多介绍 vue-demi 的使用的文章,但是都没有问题的解决方案,所以在这里记录一下我踩过的坑,希望大家之后少踩点坑。

网上已经有了很多 vue-demi 的使用文章,所以这里不介绍使用的方法了,主要记录一下踩的坑和解决的方案。

主要问题

在开发过程中,主要遇到了以下几个问题

  1. 开发过程中怎么能在 vue2vue3 环境下做快速的切换
  2. 组件使用 template 模版写,在 vue2 环境中报错
  3. 设置 img 标签的 src 属性,在 vue2 中没有展示
  4. 设置元素的事件,在 vue2 中没有生效
  5. 通过 ref 获取 DOM元素或者组件实例的时候,在 vue2 中获取的是undefined
  6. 调用 js 方法渲染组件的时候,报错
  7. vue2 环境中使用组件时,composition-api 没有生效

解决方案

坑1 开发过程中怎么能在 vue2vue3 环境下做快速的切换

解决方法是:在 node_module 下安装两个版本的 vue ,分别命名为 vuevue2 ,在需要切换版本的时候,修改 node_modules 中的文件夹名,下面以 vite 项目中的解决

根目录下新建 scripts 文件夹,创建 script/swap-vue.js 文件

/* eslint-disable camelcase */
const fs = require('fs')
const path = require('path')

const Vue2 = path.join(__dirname, '../node_modules/vue2')
const DefaultVue = path.join(__dirname, '../node_modules/vue')
const Vue3 = path.join(__dirname, '../node_modules/vue3')
const vueTemplateCompiler = path.join(__dirname, '../node_modules/vue-template-compiler')
const vueTemplateCompiler2_6 = path.join(__dirname, '../node_modules/vue-template-compiler2.6')

const version = Number(process.argv[2]) || 3

useVueVersion(version)

function useVueVersion (version) {
  if (version === 3 && fs.existsSync(Vue3)) {
    resetPackageNames()
    rename(Vue3, DefaultVue)
    useTemplateCompilerVersion(3)
  } else if (version === 2 && fs.existsSync(Vue2)) {
    resetPackageNames()
    rename(Vue2, DefaultVue)
    useTemplateCompilerVersion(2)
  } else {
    console.log(`Vue ${version} is already in use`)
  }
}

function resetPackageNames () {
  if (!fs.existsSync(Vue3)) {
    rename(DefaultVue, Vue3)
  } else if (!fs.existsSync(Vue2)) {
    rename(DefaultVue, Vue2)
  } else {
    console.error('Unable to reset package names')
  }
}

function useTemplateCompilerVersion (version) {
  if (!fs.existsSync(vueTemplateCompiler)) {
    console.log('There is no default vue-template-compiler version, finding it')
    rename(vueTemplateCompiler2_6, vueTemplateCompiler)
    console.log('Renamed "vue-template-compliler2.6" to "vue-template-compliler"')
  }
  if (version === 3 && fs.existsSync(vueTemplateCompiler)) { 
    rename(vueTemplateCompiler, vueTemplateCompiler2_6)
  }
}

function rename (fromPath, toPath) {
  if (!fs.existsSync(fromPath)) return
  try {
    fs.renameSync(fromPath, toPath)
    console.log(`Successfully renamed ${fromPath} to ${toPath} .`)
  } catch (err) {
    console.log(err)
  }
}

package.json 添加下面的执行命令

{
    "scripts": {
        "use-vue:2": "node scripts/swap-vue.js 2 && vue-demi-switch 2",
        "use-vue:3": "node scripts/swap-vue.js 3 && vue-demi-switch 3",
        "dev:v2": "pnpm run use-vue:2 && VUE_VERSION=2 vite",
        "dev:v3": "pnpm run use-vue:3 && VUE_VERSION=3 vite"
    },
    "devDependencies": {
        "@vitejs/plugin-vue": "^4.1.0",
        "vite": "^4.3.2",
        "vite-plugin-vue2": "^2.0.3",
        "vue": "^3.2.47",
        "vue-template-compiler2.6": "npm:vue-template-compiler@2.6.11",
        "vue2": "npm:vue@2.6.11"
    },
    "dependencies": {
        "vue-demi": "^0.14.5"
    }
}

这样在之后执行 pnpm run dev:v2 就是 vue2 环境,执行 pnpm run dev:v3 就是 vue3 环境

该方案参考 :GitHub - vuelidate/vuelidate

因为 vite 在编译 vue2vue3 的时候,需要使用到不同的插件,所以还需要在 vite.config.js 中做判断。

vite 编译 vue2 用到插件 vite-plugin-vue2,下载

pnpm add vite-plugin-vue2 -D

修改 vite.config.js

// 通过 package.json 中设置的环境变量判断是不是 vue2 环境
const isVue2 = +(process.env.VUE_VERSION) === 2;

export default defineConfig(async () => {
  return ({
    plugins: [
      // 如果不是 vue2 环境引入 vite-plugin-vue2 插件会有报错,所以这里做判断之后在引入
      isVue2 ? (await import("vite-plugin-vue2")).createVuePlugin() : vue(),
    ]
  })
})

坑2 组件使用 template 模版写,在 vue2 环境中报错

因为 vue2vue3template 模版生成的 render 函数不一样,所以不能使用 template 写,做不了兼容。

解决方案:用 render 函数或 setup 中返回 render 函数,示例使用 setup 返回一个 render 函数

import { h } from "vue-demi";

const Toast = {
  props: {
    text: {
      type: String,
      default: ""
    }
  },
  setup(props) {
    return () =>
      h("div", [
        h("section", { class: 'toast-container' }, [
          h("div", { class: 'toast' }, [
            h("slot", `${props.text}`)
          ])
        ])
      ])
  }
}

坑3 设置 img 标签的 src 属性,在 vue2 中没有展示

因为 vue2 中的 vnodevue3 中不一样,在 vue3 中设置 imgsrc 可以直接通过 src 设置,在 vue2 中则要通过 attrs.src 设置。

// vue3
h("img", {src: "xxx"})
// vue2 
h("img", { attrs: { src: "xxx" } })

坑4 设置元素的事件,在 vue2 中没有生效

和设置 src 属性一样,在vue3 中设置元素事件是直接通过 on${事件} 设置的,在 vue2 中需要通过 on.${事件} 设置

// 设置 click 事件
// vue3
h("div", {onClick: () => {})
// vue2
h("div", { on: { click: () => {} } })

这里为了不需要在每个h函数的调用中处理这些问题,所以写一个函数统一处理了

import { isVue2 } from 'vue-demi'
const attrsNames = ['src'];

export function transformVNodeProps(props) {
  if (!isVue2) { return props }
  const on = {};
  const attrs = {};
  const events = Object.keys(props)
    .filter(event => /^on[A-Z]/.test(event))
    .forEach(event => {
      const eventName = event[2].toLowerCase() + event.substring(3);
      on[eventName] = props[event];
    })

  props.on = Object.assign({}, on, props.on || {});
  attrsNames
    .filter(name => props[name] !== undefined)
    .forEach(name => {
      attrs[name] = props[name]
    })
  props.attrs = Object.assign({}, attrs, props.attrs || {})
  return props;
}

之后在调用h 函数的时候,传入的 props 都用 vue3 的方式写就可以了。

// vue2 / vue3
h("div", transformVNodeProps({ src: "xxx", onClick: () => {} }))

坑5 通过 ref 获取 DOM元素或者组件实例的时候,在 vue2 中获取的是undefined

这个是因为 composition-api 导致的

参考:GitHub - vuejs/composition-api: Composition API plugin for Vue 2

解决方案:在onMounted 生命周期中通过 setupContext.refs 获取

{
    setup(props, setupContext) {
        const container = ref()
        const refs = setupContext.refs;
        if (isVue2) {
            onMounted(() => { container.value = refs.container })
        }
        return () => h("div", { ref: isVue2 ? "container", container })
    }
}

坑6 调用 js 方法渲染组件的时候,报错

在需要通过 js 方法渲染组建的时候,如果可以使用createApp 完成需求,就尽量不要使用 vue2Vue.extend 或者 vue3render 函数,避免处理复杂的兼容性问题。

import { createApp, isVue2 } from 'vue-demi'
import TestComponent from './test-component'
let instance;
let app;
let container;
async function jsRender() {
  if (instance) { return }
  return new Promise((resolve) => {
      const remove = () => {
          app.unmount();
          document.body.removeChild(isVue2 ? instance.$el : container)
          app = null;
          instance = null;
          container = null;
      }
      app = createApp(TestComponent)
      container = document.createElement('div');
      document.body.appendChild(container)
      instance = app.mount(container)
  })
}

坑7 在vue2 环境中使用组件时,composition-api 没有生效

vue2 环境中,要使用 composition-api 需要通过 vue.use(VueCompositionAPI) 函数注册插件之后,才能使用。

vue-demi 中导出了 install 函数就是完成这个操作的。

vue-demi 会默认执行一次 install 函数,但是这个函数并没有把 VueCompositionAPI 挂载到我们 vue2 项目中使用的 Vue 上,而是挂载在它自己引入的 Vue 上。

为了 VueCompositionAPI 能正确的挂载,需要在我们插件导出的 install 中手动执行一次 install 函数。

impprt { install } from 'vue-demi'
export default (_vue) => {
    install(_vue)
})

总结

以上内容就是在这次 vue-demi 开发插件中遇到的问题了,解决问题的过程也是学习的过程,看了很多优秀的开源项目是怎么解决这些遇到的。

大家如果还有遇到的问题和解决方案的,欢迎留言补充。