项目vue2升级vue3
Vue2迁移vue3操作指南
一、前言
有个自动化迁移工具gogocode-cli,尝试后发现不好用且得不偿失,就放弃了,感兴趣的可以去了解一下,本指南选择手动迁移
迁移开始之前,我们先来梳理下思路:
现在有一个vue2的项目,
首先我们升级框架,
得到了一个vue3的框架,但是上面放着vue2的代码,跑不起来,
然后我们安装并配置迁移构建版本(@vue/compat),
通过它能够在vue3的框架上跑绝大多数vue2的代码(兼容与否可自行配置)
接下来我们逐个迁移,将vue2的代码替换成vue3的写法,
如果项目中有必须依赖vue2的东西,就只能通过迁移构建版本兼容,
如果能完全vue3化,则迁移完毕后,删除迁移构建版本
注意,如果跟着本文档一步步迁移,则中途可以先不运行项目,将已知的迁移步骤完成后,再运行项目,解决剩余没有提及到的可能存在的问题
二、迁移过程
1.升级依赖
修改package.json文件(仅供参考,如果你项目用了不同的依赖配置,升级成适配vue3的就行了)
- 升级vue、vuex、vue-router、@vue/cli-service
- 新增@vue/compat
- 替换element-ui 为 element-plus
- 替换vue-template-compiler 为 @vue/compiler-sfc
- 删除@babel/plugin-transform-runtime (因为@vue/cli-plugin-babel里本就包含了它)
然后删除node_modules,删除lock文件,重新执行npm install
这里可以先暂时不考虑其他依赖的升级适配,放到后面进行
参考代码:
"dependencies": {
"@vue/compat": "^3.2.33",
"element-plus": "^2.2.2",
"vue": "^3.2.33",
"vue-router": "^4.0.15",
"vuex": "^4.0.2"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~5.0.4", // cli相关工具升级
"@vue/cli-plugin-eslint": "~5.0.4",
"@vue/cli-service": "~5.0.4", //
"@vue/compiler-sfc": "^3.2.33", // "vue-template-compiler": "^2.6.10"
"eslint": "^7.32.0",
"eslint-plugin-vue": "^7.0.0" // 适配vue3的话要升级到7以上
}
2.切换迁移构建版本
首先在vue.config.js文件中配置迁移构建版本
MODE值为3时,默认所有兼容配置关闭(即false),有要兼容vue2的,需将对应配置项定义为true
MODE值为2时,默认所有兼容配置开启(即true),不需要兼容vue2的,需将对应配置项定义为false
注意,迁移构建版本只是兼容大多数vue2写法,但还是有些地方无法兼容(如过滤器、插槽),必须适配vue3写法
参考代码:
// 对vue cli内部的webpack配置
chainWebpack: config => {
// 别名
config.resolve.alias.set('vue', '@vue/compat')
// 配置模式
config.module
.rule('vue')
.use('vue-loader')
.tap(options => {
return {
...options,
compilerOptions: {
// 此处的配置是特定于编译器的,
// 如果您使用的是完整版本(使用浏览器内编译器),则可以在运行时配置它们。
// 但是,如果使用构建设置,则必须通过compilerOptions构建配置中的来配置它们
compatConfig: {
MODE: 3 // MODE: 2
}
}
}
})
}
然后新增兼容配置文件configureCompat.js并在入口文件中引入
参考代码:
import { configureCompat } from 'vue'
configureCompat({
MODE: 3 // MODE: 2
})
3.Eslint调整
同样这里仅作参考,如果你项目用了别的eslint依赖,升级成适配vue3的就行了
项目中有使用 eslint-plugin-vue 的,在vue3中需升级到7以上
如下图源码所示,可以看见,只有7以上版本才适配了vue3的规则
所以请先确保插件 eslint-plugin-vue 的版本在7以上,然后根据需要选择上图右侧configs里合适的配置项
这里我们将.eslintrc.js文件中的 “plugin:vue/essential” 替换为 “plugin:vue/vue3-essential”
此外,建议将vscode的vetur插件替换成volar插件,以支持vue3语法
如果说vue2的官配是vetur,那么vue3的官配就是volar, 请对使用vue3的项目局部禁用vetur、局部启用volar,使之不影响vue2项目的使用
4.babel补充说明
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
// 可以用 '@vue/app' 它是 @vue/babel-preset-app 的缩写
// 或者用 '@babel/preset-env' 不过就要搭配 @babel/plugin-transform-runtime
]
// plugins: ['@babel/plugin-transform-runtime']
}
// @vue/cli-plugin-babel 是 vue-cli 的 babel 插件,
// 它默认使用 Babel 7 + babel-loader + @vue/babel-preset-app
// @vue/babel-preset-app 是 vue-cli 默认的 babel 预设,
// 它包含 @babel/preset-env 和 @babel/plugin-transform-runtime
// @babel/preset-env 的作用是根据浏览器目标自动确定要应用的转换和 polyfill ,
// 它的 targets 默认为 .browserslistrc 文件内的配置
// @babel/plugin-transform-runtime 会将 helper 和 polyfill 都改为从一个统一的地方引入,
// 并且引入的对象和全局变量是完全隔离的,避免了对全局变量及其原型的污染
5.Webpack5相关调整
webpack5 移除了disabledHostCheck属性
在vue.config.js文件里,将 “disabledHostCheck: true” 替换为 allowedHosts: 'all'
6.全局API调整
使用createApp 创建应用实例app,然后根据如下图所示的API,依次全局搜索并替换
上图右侧的“app.$mount”写错了,在vue3中挂载实例写法应为“app.mount”
7.调整vuex、vue-router
【vuex】
改用“createStore”方法创建实例,然后在bootstrap.js中引入并传递给use
【vue-router】
① 改用“createRouter”方法创建实例,然后在bootstrap.js中引入并传递给use
use方法要放在mount挂载之前
② 原本的“mode”被“history”配置取代,它的值可为:
- createWebHistory() // 对应“history”
- createWebHashHistory() // 对应“hash”
- createMemoryHistory() // 对应“abstract”
③ 移动了“base”,现作为上述3个方法的第一个参数,如createWebHistory(‘XXX’)
④ 删除通配符路由,请将路由中的“”改为“:pathMatch(.)*”
⑤ <transition>和<keep-alive>必须通过v-slot才能在<router-view>内使用
全局搜“<router-view”,如果有如下写法:
<keep-alive> // 或 <transition>
<router-view>
<p>xxx</p>
</router-view>
</keep-alive> // 或 </transition>
请改造成:
<router-view v-slot='{Component}'>
<keep-alive> // 或 <transition>
<component :is='Component'>
<p>xxx</p>
</component>
</keep-alive>
</router-view> // 或 </transition>
举例如下:
⑥ 全局搜索“history.pushState”,有的话
//将
history.pushState(XXX, '', url)
//替换成
await router.push(url)
history.replaceState({ ...history.state, ...XXX}, '')
⑦ 全局搜索“history.replaceState”,有的话
//将
history.replaceState({}, '', url)
//替换成
history.replaceState(history.state, '', url)
⑧ 删除<router-link>中的“append”、“event”、“tag”、“exact”属性
全局搜索“<router-link”,依次查看
有用到“append”的,删除,并将“to”中的路径改成完整路径
有用到“exact”的,直接删除
有用到“event”、“tag”的,使用v-slot改写
例如:
<!-- 将 -->
<router-link to="/about" tag="span" event="click">About Us</router-link>
<!-- 改写成 -->
<router-link to="/about" custom v-slot="{ navigate }">
<span @click="navigate">About Us</span>
</router-link>
⑨ 带有空 path 的命名子路由不再添加斜线,这会影响子集的redirect ,举个例子:
{
Path: '/XXX',
Children:[
// vue2中重定向到“/XXX/yyy”, vue3中重定向到“/yyy”
{ path: '' , name: 'test', redirect: 'yyy'} ,
{ path: 'yyy' }
]
}
⑩ 其他
- “pathToRegexpOptions” 属性被 createRouter() 中的 “sensitive” 取代
- “caseSensitive” 属性被 createRouter() 中的 “strict” 取代
- 将 “router.onReady(onSuccess, onError)” 改为 “isReady().then(onSuccess).catch(onError)” 的写法
- 将 “scrollBehavior” 中返回对象的 “x” 改为 “left”,“y” 改为 “top”
- 忽略 mixins 中的导航守卫
- 删除 “fallback” 属性,因为现在vue支持的浏览器都支持history模式
- 删除 “router.match”,合并到 “router.resolve” 中
- 删除 “router.getMatchedComponents()”
- 删除 “router.app”
- 删除路由地址中的 “parent” 属性
- 所有的导航现在都是异步的
- 取消了path-to-regexp,所以不再支持未命名的参数
- 跳转或解析不存在的命名路由或缺少params参数会报错(以前是会导航到'/')
8.element-ui 替换成 element-plus
① 全局搜索 “element-ui”,替换插件,改造如下
② 主题文件路径更换
将“~element-ui/packages/theme-chalk/src/index” 替换为 “~element-plus/theme-chalk/src/index”
将“element-ui/lib/theme-chalk/index.css” 替换为 “element-plus/theme-chalk/index.css”
③ 所有标签的size属性的值从“medium / small / mini”变更为“large / default /small”
全局搜索“size="medium"”,替换为“size="large"”
全局搜索“size="small"”,替换为“size="default"”
全局搜索“size="mini"”,替换为“size="small"”
④ <el-button>移除了“type=”text””,想维持vue2的样式,可用link属性代替
全局搜索“type="text"”,依次将<el-button>上的它替换为“type="primary" link”
不要将例如<input type=”text”>这样的改跑了
⑤ icon图标调整
element-ui是通过类名使用,如对于搜索图标是class=”el-icon-search”
而element-plus是当做组件使用,如对于搜索图标是<el-icon><Search/></el-icon>
使用之前,需要下载依赖
分支目录下命令行输入“npm install @element-plus/icons-vue”
然后全局引入,配置如下:
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
Object.keys(ElementPlusIconsVue).forEach(key => {
app.component(key, ElementPlusIconsVue[key])
})
全局搜索“el-icon”,然后根据使用方法做出相应调整,举例如下:
- 对于loading里的spinner参数,如非必要,建议删除
因为(截止到目前)官网说使用类名,但类名已经废了,所以没找到改法
- 对于通过class使用的,改成组件的形式,具体名称对比两者官网
大概率是el-icon-后面的名称的大驼峰写法
原有属性请放到<el-icon>中,icon组件会渲染成<svg>标签
项目中有可能针对“el-icon-xxx”类名设置了样式,所以建议保留原类名
- 对于<el-input>、<el-button>上的做如下修改
⑥ <el-row>的type属性删除(element-plus使用flex布局,不用专门设置)
全局搜索“<el-row”或“type=”flex””
⑦ Date Picker / Time Picker / DateTime Picker 日期时间选择器改动
- “first-day-of-week”属性删除(官方建议用dayjs里的方法代替,具体请自查)
- “picker-options”属性删除,新增属性shortcuts、disabled-date、cell-class-name
⑧ 名称改动
全局搜索“open-delay”,替换成“show-after”
全局搜索“close-delay”,替换成“hide-after”
全局搜索“ElSubmenu”,替换成“ElSubMenu”
全局搜索“el-submenu”,替换成“el-sub-menu”
⑨ 对话框改动
全局搜索“<el-dialog”,
将它上面的“:visible.sync”改写成“v-model”,
将它上面的“:visible”改写成“:model-value”
⑩ 表单校验的调整(主要针对required)
如果表单配置了rules或required(值为null也算),就一定要有prop
除非将<el-form-item>内表单组件的validate-event属性设为false,单独关闭校验
至于为什么说required值为null也算,我们看下element-plus的源码
因为void 0 === undefined ,所以,
可以看到,当required的值不为这两个时,会赋值成false放进rules里
全局搜索“required”
首先将<FormItem>这种公共组件内部的required默认值定义为undefined或void 0
因为required: Boolean这种写法,不传值进来的话,默认值是false
然后找出所有<el-form-item>或<FormItem>标签上用了required但没有prop的地方
也就是没有prop说明没有校验的需求,仅为了显示 *
对于这种情况,这里选择将required删了,添加自定义类名,自己实现 *
9.异步组件调整
这里说的异步组件,不包括vue-router的懒加载,不用改它们
用了异步组件写法的,先看下是否真的有必要作为异步组件使用,如果只是为了直接写在components里方便,建议改成普通组件写法
举个例子,使用模块联邦的远程模块需异步加载,
但如果是在vue.config.js文件中用“remotes”配置的,那么它本就是异步加载,
使用时,像普通组件一样使用就行,
没用“remotes”选项配置的,才需要自己写异步加载
如下图所示,该项目使用了“remotes”
故无需再用异步引入,修改如下:
对于真的使用异步组件的,则需包裹“defineAsyncComponent”方法,如果使用了带选项的异步组件,选项里的component改名为loader
参考代码:
// vue2
components: {
XXX: () => import('AAAA/BBB'),
XXX2: () => ({component: import('AAAA/BBB')}),
}
// vue3
import { defineAsyncComponent } from 'vue'
components: {
XXX: defineAsyncComponent( () => import('AAAA/BBB') ),
XXX2: defineAsyncComponent( {loader: () => import('AAAA/BBB')} )
}
10.$attrs、$listeners、片段
$attrs现在也包含了class和style,$listeners移除,事件监听器变成$attrs的一部分
片段允许组件有多个根节点,但是需要显示的定义v-bind="$attrs"
首先全局搜索“$listeners”,
有v-on="$listeners" 的删除,有this.$listeners 的改写成this.$attrs.onXxxx
然后全局搜索“inheritAttrs”,看下有没有inheritAttrs: false
有则表示该组件的根元素不会继承$attrs,需要显示绑定,如果绑定在子元素上,
那么看一下使用该组件时,组件标签上是否定义了class和style,
在vue2它们是落在根节点上的,而在vue3会落在绑定了$attrs的那个元素上,
所以有的话请改造,确保样式等不会受到影响
最后全局搜索“v-bind="$attrs"”
如果它写在根节点,且只有一个根节点,且又无inheritAttrs: false的设置,
则可以删除,因为默认就在根节点,
如果不在根节点上,则看下有没有受到class和style的影响
11.部分实例属性移除
$set、$delete、$children、$on、$off、$once
全局搜索它们,该删删 ,该改改
有用$children的话可以用$refs改造取代
$on、$off废弃的话Bus也就废了,有用到的话请用别的通信方式替代
Vue3用proxy代替Object.defineProperty,所以不再需要$set,请改为普通赋值写法
也可以通过自定义的全局属性重写这些方法实现原有的效果,就不用挨个修改了
顺带提一嘴.prop修饰符也移除了,用了的自行搜索解决方法
12.is属性变动
Vue3中is的渲染替换效果只在<component>标签上有用,
其他标签上的需要从“is=’xxx’”改成“is=’vue:xxx’”,否则is会被当成一个普通属性(即自定义内置元素)
全局搜索“is=”,如果有写在其他标签上的,请按上述方式改造
13.data选项变动
data现在只接收函数,并且mixins、extends的data现在变成浅合并
官方建议用组合式API代替mixins、extends
全局搜“mixins”和“extends”,看是否有data选项,有的话分析浅合并带来的改变并调整
14.filter过滤器移除
Vue3为了遵循大括号内只是js的原则,删除了过滤器,所以需要用方法或计算属性替换
为减少工作量,建议用方法替换
首先全局搜索“Vue.filter”,如果使用了全局过滤器,官方推荐通过全局属性来改造
这里我们定义一个全局属性$filters,里面存放原本的全局过滤器
使用的时候,通过$filters调用里面的过滤器方法即可
参考代码:
app.config.globalProperties.$filters = {}
Object.keys(filters).forEach(key => {
app.config.globalProperties.$filters[key] = filters[key]
})
然后全局搜索“ | ”(注意前后都有空格),定位到使用了过滤器的地方
万一有哪处没这样写,搜索时漏掉了,就以后run时报错了再改吧
文件内依次搜索过滤器名称
如果该过滤器在filters选项里定义了,说明它是局部过滤器,只需改成方法调用
否则为全局过滤器,方法前面要加上之前定义的全局属性$filters,才能调用的到
然后再将filters选项中的局部过滤器移动到methods中,并删掉filters选项
最后全局搜索“filters:”,删除filters选项
可能存在使用了filters选项但是没内容或者未使用的情况
15.渲染函数调整
render() 的作用是渲染,h() 的作用是创建vnodes,h函数现在是全局导入
vue2中是作为参数,且全名是createElement
vue3中由于VNode是上下文无关的,不能再用字符串隐式查找注册的组件
所以需要使用resolveComponent方法进行包裹
注意这里说的是参数为字符串的情况
如果h的第一个传参接收的是字符串,例如vue2的写法为“render: h => h(‘XXX’)”
那么就需要引入resolveComponent方法,改成“render: () => h(resolveComponent(‘XXX’))”
截图示例中h的第一个参数是对象,所以无需使用resolveComponent
首先全局搜索“render”,找到使用渲染函数的地方,引入h方法,然后改造
全局搜索“$createElement”,有的话,两种改法
- 第一种,引入h函数,替换“this.$createElement”
- 第二种,自定义“this.$createElement”,其余地方不变
推荐这种,改起来快,且能适配到某些插件
16.函数式组件调整
functional移除
Vue3建议使用有状态的组件,因为函数组件的优势已经可以忽略不不计
函数组件现在只能由接收props和context(slots、attrs、emit)的普通函数创建
全局搜索“functional”,看看有没有functional: true 或 <template functional>
下图为使用functional: true的改造示例,<template functional>的很少用,不做示范
17.key调整
如果在v-if、v-else、v-else-if上使用了key,则每个分支的key必须唯一
如果在<template>标签上使用了v-for,
在vue2中,<template>不能有key,解决方法是在其子节点上设置key
但是在vue3中,key应该被写在使用了v-for的这个<template>上
全局搜“<template”,看是否有在该标签上使用v-for的
有的话参考下图调整
18.按键修饰符调整
keyCode废弃,不再支持使用数字 (即键码) 作为 v-on 的修饰符
全局搜索“@keyup”或“v-on:keyup”,有的话请查看官网自行改造
19.propsData选项移除
Vue2中propsData用于在创建vue实例时传入prop,vue3移除了它,可用createApp的第二个参数代替
全局搜索“propsData”,有的话请自行改造,参考如下:
20.过渡类名修改
全局搜索“v-enter”,有的话替换为“v-enter-from”
全局搜索“v-leave”,有的话替换为“v-leave-from”
21.<transition>
当使用 <transition> 作为根结点的组件从外部被切换时将不再触发过渡效果
<transition-group> 不再默认渲染根元素,但仍然可以用 tag attribute 创建根元素
全局搜索“<transition”,有的话请查看官网自行改造
22.v-bind合并行为
Vue3中v-bind 的绑定顺序会影响渲染结果
全局搜索“v-bind="{”,如果有,请参照下图修改
23.生命周期调整
beforeDestroy 和 destroyed 已经分别被重命名为 beforeUnmount 和 unmounted
全局搜索“beforeDestroy”,替换为“beforeUnmount”
全局搜索“destroyed”,替换为“unmounted”
全局搜索“hook:”,替换成“vnode-”
24.v-model和.sync调整
默认prop从“value”变成“modelValue”
默认event从“input”变成“update:modelValue”
.sync修饰符移除
model选项移除
同一个组件可以有多个v-model绑定
可以自定义v-model修饰符
【使用默认v-model的写法】
- vue2 中
// 子
export default {
props: {
value: String,
},
methods:{
fun() {
this.$emit('input', 'XXX')
}
}
}
<!-- 父 -->
<Child v-model='pageTitle'>
<!--可能存在 <Child v-model='pageTitle' @input='YYY' @success='ZZZ'> -->
- vue3 中
// 子
export default {
props: {
modelValue: String,
},
methods:{
fun() {
this.$emit('update:modelValue', 'XXX')
// this.$emit('input', 'XXX')
// this.$emit('update:modelValue', 'XXX')
}
}
}
<!-- 父 -->
<Child v-model='pageTitle'>
<!-- // 替换 <Child v-model='pageTitle' @update:modelValue='YYY' @success='ZZZ'> -->
<!-- // 或共存 <Child v-model='pageTitle' @input='YYY' @success='ZZZ'> -->
【使用v-model搭配model选项的写法】
- vue2 中
// 子
export default {
model: {
prop: 'title',
event: 'change'
},
props: {
title: String,
},
methods:{
fun() {
this.$emit('change', 'XXX')
}
}
}
<!-- 父 -->
<Child v-model='pageTitle'>
<!-- 可能存在 <Child v-model='pageTitle' @change='YYY' @success='ZZZ'> -->
- vue3 中
// 子
export default {
// 删除model选项
props: {
title: String,
},
methods:{
fun() {
this.$emit('update:title', 'XXX')
// this.$emit('change', 'XXX')
// this.$emit('update:title', 'XXX')
}
}
}
<!-- 父 -->
<Child v-model:title='pageTitle'>
<!-- 替换 <Child v-model:title='pageTitle' @update:title='YYY' @success='ZZZ'> -->
<!-- 或共存 <Child v-model:title='pageTitle' @change='YYY' @success='ZZZ'> -->
【使用.sync的写法】
- vue2 中
// 子
export default {
props: {
title: String,
},
methods:{
fun() {
this.$emit('update:title', 'XXX')
}
}
}
<!-- 父 -->
<Child :title.sync='pageTitle'>
- vue3 中
// 子
export default {
props: {
title: String,
},
methods:{
fun() {
this.$emit('update:title', 'XXX')
}
}
}
<!-- 父 -->
<Child v-model:title='pageTitle'>
先明确一点,对于v-model,我们要改造的是自定义组件(项目里自己写的)上的
首先全局搜索“$emit(‘input”和“$emit(“input”
单双引号、空格等情况要考虑到 如有,且组件内还用到了“value”这个prop,再根据组件名找到所以使用它的地方,看下是否用的默认v-model,是就改造
然后全局搜索“model: {”,找出所有使用model选项的自定义组件
再分别找出所有使用了它们的地方,确定它们用了v-model,然后改造
举例个特殊情况,如下图
可以看到,change事件监听器执行了2个方法,v-model自带的和额外定义的
两种改法,一种是维持1个监听器的效果:
另一种是变成2个监听器,原有的change事件保持,在它旁边增加新的监听器
最后全局搜索“.sync”,有的话,将“:XXX.sync”改写成“v-model:XXX”
25.新增emits选项,移除.native
emits用来定义子组件向其父组件触发的事件(写法类似props)
官方建议使用emits记录组件所触发的所有事件
任何未在emits记录的事件都会算入该组件的$attrs,
即作为原生事件添加到子组件的根元素(或定义了v-bind=’$attrs’ 的元素)中
也就是说,以前需要用.native实现的效果(如click事件)
现在只要不在emits里加同名事件(如click)就能实现
这里可以看下官方举的例子,便于理解:
这里有一点需要注意,
虽然官方只是建议用emits记录所有事件,不记录也会被放进$attrs作为原生事件用,
但是当自定义事件的名称与原生事件重名时,例如:$emit(‘click’)
不记录的话,事件可能会被触发两次,或者意料之外的状况,
所以,要么自定义事件命名时避开原生事件名,要么,就在emits中记录
首先全局搜索“.native”,直接点击替换,删除.native
然后全局搜索以及文件内搜索“this.$emit(”,增加emits选项,记录所有自定义事件
26.插槽调整
vue2中,$slots用来访问插槽分发的内容,$scopedSlots用来访问作用域插槽
Vue3中,移除了$scopedSlots,将插槽作为函数公开(以前是对象)
【具名插槽】
- vue2 中
<!-- 子 -->
<slot name='XXX'>
<!-- 父 -->
<template slot='XXX'>
- vue3 中
<!-- 子 -->
<slot name='XXX'>
<!-- 父 -->
<template v-slot:XXX>
<!-- 可缩写为 <template #XXX> -->
【作用域插槽】
- vue2 中
<!-- 子 -->
<slot :YYY='yyy'>
<!-- 父 -->
<template slot-scope='scope'> {{scope.YYY}}
- vue3 中
<!-- 子 -->
<slot :YYY='yyy' name='XXX'>
<!-- 父 -->
<template v-slot:XXX='scope'> {{scope.YYY}}
注意,v-slot只能写在<template>中,除非是独占默认插槽(能写在组件标签上)
首先全局搜索 “$scopedSlots”,将 “$scopedSlots” 替换为 “$slots”
再全局搜索 “$slots”,将 “$slots.XXX” 替换为 “$slots.XXX()”
请分开搜索避免遗漏,因为在vue2中他们是两个东西,只不过在vue3中统一了写法
然后全局和文件内搜索 “slot=”, 依次将 “slot='XXX'” 改成 “#XXX” 或者 “v-slot:XXX”
对于非<template>标签上的slot, 先在它的外层包裹上<template>标签,然后将slot挪到<template>上
最后全局搜索“slot-scope=”,替换成“#default=”或者“v-slot:default=”
27.穿透样式调整
样式中使用“/deep/”、“>>> ”、“::v-deep”的会报错
全局搜索它们,然后换成“:deep()”的格式
28.升级其余依赖(仅供参考)
对剩下的依赖(第1步没处理的)进行升级或替换,适配vue3,
注意需要调整的肯定是插件内部有使用到vue相关功能的, 如果有依赖只能用vue2版本的话,请记录下来,后续只能继续使用迁移构建版本
举个例子,如果使用了“@riophae/vue-treeselect”插件
分支目录下命令行输入“$ npm uninstall @riophae/vue-treeselect”
然后输入“npm install --save vue3-treeselect”
全局搜索“@riophae/vue-treeselect”
先将“@riophae/vue-treeselect/dist/vue-treeselect.css”
修改为“vue3-treeselect/dist/vue3-treeselect.css”
再将“@riophae/vue-treeselect”替换为“vue3-treeselect”
29.微前端适配
首先去除Vue的引入,然后改造singleSpaVue()的传参,
router和store已经通过use引入,所以appOptions里不用再写
将“Vue”换成“createApp”,插件会自己创建一个vue实例,
然后将实例作为参数调用handleInstance的方法(有的话),之后会将实例挂载
请在appOptions里传入el,否则插件会自己在body创一个容器,
这样的话,不管将实例挂载在哪,都会多出一个容器,影响页面展示,详情请看源码
现在我们有两个app实例了(自己创的用于单独运行项目的,以及微前端里插件创的),
我们将之前定义的一堆东西封装成一个方法,然后让两个实例都调用它
顺带一说,vue-router v4.x中的router.history已经移除了,所以这里删掉它, 可以分别打印一下v3.x和v4.x的router对比一下,也可参考官网深入解读
具体改动可参考下图:
30.其余调整
以下改动项目里会用到的概率低,可自行检查,也可作为错误排查的参考:
- v-for上有ref的,ref不再自动创建
- props默认值不能再访问this
- v-if和v-for优先级变动,在vue2中v-for高,vue3中v-if高
- attribute 强制行为改动(值为false的处理方式有改动,具体官网自查)
- 挂载有template的应用时,从vue2的替换元素变成vue3的作为子元素插入
- 没有特殊指令的 <template> 现在会被渲染为原生元素
到这里,常规的迁移工作就完成了, 接下来请尝试 lint 一下或直接运行项目, 如果还有报错或警告,请逐一检查,需要修复的就修复(第32步有举例可参考), 修复完成后,如果项目中没有必须依赖vue2的地方, 就可以去掉迁移构建版本,切换成vue3版本,否则只能继续使用迁移构建版本, 之后,请对系统功能做测试验证,测试所有组件及交互, 可能还会有错误或警告,再做修复
31.切换vue3版本
确定项目没有依赖vue2的地方后,我们开始尝试去掉迁移构建版本
建议先注释掉,后续验证完系统功能,确定vue3迁移成功后,再删除
32.错误整理(仅供参考)
① 命令行可能会发现报错Unexpected mutation of "XXX" prop
这是eslint对单向数据流的检测,子组件直接修改了prop就会报这个错
如果是个新项目,那么以它为约束进行开发是很好的, 但是如果现有的项目已经有写成这样不规范的且运行正常,就不要轻易改动了, 代价大且容易产生新的风险
这里我们在.eslintrc.js文件中配置“'vue/no-mutating-props': 'off'”关闭这个检测
② 无关的非props属性传递给组件带来的警告
如果往组件传递了个未被定义属性(如下图的visible),且它又不是$attrs里带的(如title),
就会报这样的警告,大概率是代码有误
比如下图为<el-dialog>中写成“v-model:visible”所报的错,解决办法是写作“v-model”
③ 无关的非emits事件监听器传递给组件带来的警告
如果往组件传递了个未被定义属性(如下图的beforClose),且它又不是$attrs里带的(如input),
就会报这样的错,大概率是代码有误
比如下图,
“@befor-close”是错误写法,正确用法是“:befor-close”,
所以解决方法是删掉<el-dialog>上的“@befor-close”
④ 使用了废弃属性带来的警告
查看“vue3-treeselect”源码后发现,它使用了functional(vue3已移除),
完全切换成vue3版本后就不会有这个警告了,所以忽略这个警告
⑤ 解析不了标签带来的警告
不影响使用,忽略这个警告
⑥ attribute 强制行为改动带来的警告
如下图所示,可以看到“aria-current”这个属性(li标签上的无障碍属性),
再对比下vue3中对于attribute 强制行为的改动,
也就是说,对于“aria-current”,element-plus是希望,当它的值为false时,
输出为“aria-current=‘false’”,而不是移除
完全切换成vue3版本后就不会有这个警告了,所以忽略这个警告
⑦ 使用未定义的实例属性带来的警告(大概率是代码有误)
下图的例子是因为vue2中的“this.$createElement”也就是h函数,
在vue3中变成了通过import使用,“this.$createElement”也删除了
改法在第15点说过了,这里是插件的问题(用了但没完全用),建议用第二种改法
三、后记
别忘了学习Vue3的组合式API
官方推荐vue3的项目使用vite代替vuecli,
但是考虑到迁移成本、兼容性以及社区生态等因素,短期内还是推荐继续使用vuecli
Vuecli迁移vite教程,感兴趣的可以了解一下