背景介绍
接到了一个需求,在多个系统上添加同一个功能,这多个系统中包含了 Vue2
和 Vue3
,为了多系统之间的复用,决定开发一个 Vue2
和 Vue3
都可以集成的插件
vue-demi
插件天生就是为了帮助完成这件事情的。
github(vue-demi)
在开发中遇到了很多问题,看了很多介绍 vue-demi
的使用的文章,但是都没有问题的解决方案,所以在这里记录一下我踩过的坑,希望大家之后少踩点坑。
网上已经有了很多 vue-demi
的使用文章,所以这里不介绍使用的方法了,主要记录一下踩的坑和解决的方案。
主要问题
在开发过程中,主要遇到了以下几个问题
开发过程中怎么能在 vue2
和 vue3
环境下做快速的切换
组件使用 template
模版写,在 vue2
环境中报错
设置 img
标签的 src
属性,在 vue2
中没有展示
设置元素的事件,在 vue2
中没有生效
通过 ref
获取 DOM元素或者组件实例的时候,在 vue2
中获取的是undefined
调用 js
方法渲染组件的时候,报错
在vue2
环境中使用组件时,composition-api
没有生效
解决方案
坑1 开发过程中怎么能在 vue2
和 vue3
环境下做快速的切换
解决方法是:在 node_module
下安装两个版本的 vue
,分别命名为 vue
和 vue2
,在需要切换版本的时候,修改 node_modules
中的文件夹名,下面以 vite
项目中的解决
根目录下新建 scripts
文件夹,创建 script/swap-vue.js
文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
| 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
添加下面的执行命令
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| { "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
在编译 vue2
和 vue3
的时候,需要使用到不同的插件,所以还需要在 vite.config.js
中做判断。
vite
编译 vue2
用到插件 vite-plugin-vue2
,下载
1
| pnpm add vite-plugin-vue2 -D
|
修改 vite.config.js
1 2 3 4 5 6 7 8 9 10 11
| const isVue2 = +(process.env.VUE_VERSION) === 2;
export default defineConfig(async () => { return ({ plugins: [ isVue2 ? (await import("vite-plugin-vue2")).createVuePlugin() : vue(), ] }) })
|
坑2 组件使用 template
模版写,在 vue2
环境中报错
因为 vue2
和 vue3
对 template
模版生成的 render
函数不一样,所以不能使用 template
写,做不了兼容。
解决方案:用 render
函数或 setup
中返回 render
函数,示例使用 setup
返回一个 render
函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| 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
中的 vnode
和 vue3
中不一样,在 vue3
中设置 img
的 src
可以直接通过 src
设置,在 vue2
中则要通过 attrs.src
设置。
1 2 3 4
| h("img", {src: "xxx"})
h("img", { attrs: { src: "xxx" } })
|
坑4 设置元素的事件,在 vue2
中没有生效
和设置 src
属性一样,在vue3
中设置元素事件是直接通过 on${事件}
设置的,在 vue2
中需要通过 on.${事件}
设置
1 2 3 4 5
|
h("div", {onClick: () => {})
h("div", { on: { click: () => {} } })
|
这里为了不需要在每个h
函数的调用中处理这些问题,所以写一个函数统一处理了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| 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
的方式写就可以了。
1 2
| 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
获取
1 2 3 4 5 6 7 8 9 10
| { 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
完成需求,就尽量不要使用 vue2
的 Vue.extend
或者 vue3
的 render
函数,避免处理复杂的兼容性问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| 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, { listeners: { onClose: () => { resolve('close') remove(); }, onGoToNavigation: () => { resolve('goto-navigation'); remove(); } } }) 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
函数。
1 2 3 4
| impprt { install } from 'vue-demi' export default (_vue) => { install(_vue) })
|
总结
以上内容就是在这次 vue-demi
开发插件中遇到的问题了,解决问题的过程也是学习的过程,看了很多优秀的开源项目是怎么解决这些遇到的。
大家如果还有遇到的问题和解决方案的,欢迎留言补充。