vue2 迁移vue3项目改动点(坑)

1,794 阅读14分钟

一、前言

有个自动化迁移工具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.attrsattrs、listeners、片段

attrs现在也包含了classstyleattrs现在也包含了class和style,listeners移除,事件监听器变成attrs的一部分片段允许组件有多个根节点,但是需要显示的定义vbind="attrs的一部分 片段允许组件有多个根节点,但是需要显示的定义v-bind="attrs"

首先全局搜索“listeners”,有von="listeners”, 有v-on="listeners" 的删除,有this.listeners的改写成this.listeners 的改写成this.attrs.onXxxx

然后全局搜索“inheritAttrs”,看下有没有inheritAttrs: false
有则表示该组件的根元素不会继承attrs,需要显示绑定,如果绑定在子元素上,那么看一下使用该组件时,组件标签上是否定义了classstyle,在vue2它们是落在根节点上的,而在vue3会落在绑定了attrs,需要显示绑定,如果绑定在子元素上, 那么看一下使用该组件时,组件标签上是否定义了class和style, 在vue2它们是落在根节点上的,而在vue3会落在绑定了attrs的那个元素上,
所以有的话请改造,确保样式等不会受到影响

最后全局搜索“v-bind="$attrs"”
如果它写在根节点,且只有一个根节点,且又无inheritAttrs: false的设置,
则可以删除,因为默认就在根节点,
如果不在根节点上,则看下有没有受到class和style的影响

 

11.部分实例属性移除

setset、delete、childrenchildren、on、offoff、once
全局搜索它们,该删删 ,该改改

有用children的话可以用children的话可以用refs改造取代

onon、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,里面存放原本的全局过滤器使用的时候,通过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 或