一、前言
有个自动化迁移工具gogocode-cli,尝试后发现不好用且得不偿失,就放弃了,感兴趣的可以去了解一下,本指南选择手动迁移
迁移开始之前,我们先来梳理下思路:
现在有一个vue2的项目,
首先我们升级框架,
得到了一个vue3的框架,但是上面放着vue2的代码,跑不起来,
然后我们安装并配置迁移构建版本(@vue/compat),
通过它能够在vue3的框架上跑绝大多数vue2的代码(兼容与否可自行配置)
接下来我们逐个迁移,将vue2的代码替换成vue3的写法,
如果项目中有必须依赖vue2的东西,就只能通过迁移构建版本兼容,
如果能完全vue3化,则迁移完毕后,删除迁移构建版本
注意,如果跟着本文档一步步迁移,则中途可以先不运行项目,将已知的迁移步骤完成后,再运行项目,解决剩余没有提及到的可能存在的问题
总结:1.依赖升级兼容 2.vue 全局更改:{ 路由,new Router=>createRouter 全局注册 new Vue=>createApp 3.vue3 与vue2写法区别 生命周期写法 多根组件 组合式api 移除全局熟悉$on,过滤器
二、迁移过程
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的,需将对应配置项open in new window定义为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(. )*”
⑤ 和必须通过v-slot才能在内使用
全局搜“<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)
⑧ 删除中的“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"”
④ 移除了“type=”text””,想维持vue2的样式,可用link属性代替
全局搜索“type="text"”,依次将上的它替换为“type="primary" link”
不要将例如这样的改跑了
⑤ icon图标调整
element-ui是通过类名使用,如对于搜索图标是class=”el-icon-search”
而element-plus是当做组件使用,如对于搜索图标是
使用之前,需要下载依赖
分支目录下命令行输入“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-后面的名称的大驼峰写法
原有属性请放到中,icon组件会渲染成标签
项目中有可能针对“el-icon-xxx”类名设置了样式,所以建议保留原类名
- 对于、上的做如下修改
⑥ 的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
除非将内表单组件的validate-event属性设为false,单独关闭校验
至于为什么说required值为null也算,我们看下element-plus的源码
因为void 0 === undefined ,所以,
可以看到,当required的值不为这两个时,会赋值成false放进rules里
全局搜索“required”
首先将这种公共组件内部的required默认值定义为undefined或void 0
因为required: Boolean这种写法,不传值进来的话,默认值是false
然后找出所有或标签上用了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.listeners、片段
listeners移除,事件监听器变成attrs"
首先全局搜索“listeners" 的删除,有this.attrs.onXxxx
然后全局搜索“inheritAttrs”,看下有没有inheritAttrs: false
有则表示该组件的根元素不会继承attrs的那个元素上,
所以有的话请改造,确保样式等不会受到影响
最后全局搜索“v-bind="$attrs"”
如果它写在根节点,且只有一个根节点,且又无inheritAttrs: false的设置,
则可以删除,因为默认就在根节点,
如果不在根节点上,则看下有没有受到class和style的影响
11.部分实例属性移除
delete、on、once
全局搜索它们,该删删 ,该改改
有用refs改造取代
off废弃的话Bus也就废了,有用到的话请用别的通信方式替代
Vue3用proxy代替Object.defineProperty,所以不再需要$set,请改为普通赋值写法
也可以通过自定义的全局属性重写这些方法实现原有的效果,就不用挨个修改了
顺带提一嘴.prop修饰符也移除了,用了的自行搜索解决方法
12.is属性变动
Vue3中is的渲染替换效果只在标签上有用,
其他标签上的需要从“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调用里面的过滤器方法即可
参考代码:
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 或