《Vue2 升级到 Vue3 记录》
# 背景
领导想在现在的项目中使用 ts,并且后期还想把 vue2 升级到 vue3。但是 vue2 对于 ts 不是特别友好,鉴于后面还是要升级 ts,所以还是先把 vue 升级了,后面再支持 ts。
# 技术栈调整
目前项目的技术栈是使用 vue-cli
搭建的,相关依赖都是匹配 vue2.0
版本。为了匹配 vue3.0
版本,部分技术栈需要升级,主要有以下调整:
旧版本 | 新版本 | 新版本官网介绍 | 迁移帮助文档 | |
---|---|---|---|---|
vue | 2.6.11 | 3.2.29 | https://v3.cn.vuejs.org/guide/introduction.html | https://v3.cn.vuejs.org/guide/migration/migration-build.html |
vuex | 3.6.0 | 4.0.2 | https://vuex.vuejs.org/ | https://vuex.vuejs.org/guide/migrating-to-4-0-from-3-x.html |
vue-router | 3.3.4 | 4.0.12 | https://router.vuejs.org/zh/guide/ | https://router.vuejs.org/zh/guide/migration/index.html#%E7%A0%B4%E5%9D%8F%E6%80%A7%E5%8F%98%E5%8C%96 |
element | element-ui 2.14.0 | element-plus 2.0.1 | https://element-plus.gitee.io/zh-CN/guide/design.html | |
echarts | 4.9.0 | 5.1.2 | https://echarts.apache.org/zh/option.html#title | |
vuedraggable | 2.24.3 | 4.1.0 | https://github.com/SortableJS/vue.draggable.next |
# 依赖更新
开始前先仔细阅读官方的迁移介绍 - 用于迁移的构建版本
# 1. 升级工具
- 如果使用了自定义的
webpack
设置:将vue-loader
升级至^16.0.0
。 - 如果使用了
vue-cli
:通过vue upgrade
升级到最新的 @vue/cli-service。
# 2. vue 升级到 3.x
在 package.json
将 vue
更新到 3.1,安装相同版本的 @vue/compat
。且如果存在 vue-template-compiler
的话,将其替换为 @vue/compiler-sfc
。
"dependencies": {
- "vue": "^2.6.11",
+ "vue": "^3.2.29", // element-plus 需要匹配该版本
+ "@vue/compat": "^3.2.30"
...
},
"devDependencies": {
- "vue-template-compiler": "^2.6.12"
+ "@vue/compiler-sfc": "^3.2.30"
}
2
3
4
5
6
7
8
9
10
11
在 vue.config.js
中,为 vue
设置别名 @vue/compat
,且通过 Vue 编译器选项开启兼容模式
// vue.config.js
module.exports = {
chainWebpack: config => {
config.resolve.alias.set('vue', '@vue/compat')
config.module
.rule('vue')
.use('vue-loader')
.tap(options => {
return {
...options,
compilerOptions: {
compatConfig: {
MODE: 2
}
}
}
})
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
在 main.js
调整挂载方式
import Vue from "vue";
import { createApp } from "vue";
import App from "@/App.vue";
const app = createApp(App);
app.mount("#app");
2
3
4
5
6
# 3. vue-router 升级到 4.x
package.json
"dependencies": {
- "vue-router": "3.3.4",
+ "vue-router": "^4.0.12",
...
}
2
3
4
5
src/router/index.js
// 4.x 版本写法
import {
createRouter as _createRouter,
createWebHashHistory
} from "vue-router";
function createRouter() {
return _createRouter({
history: createWebHashHistory(),
routes: [...]
});
}
const router = createRouter();
export default router;
// 3.x 版本写法
// import Vue from "vue";
// import VueRouter from "vue-router";
// Vue.use(VueRouter);
// const routes = [...];
// const router = new VueRouter({
// routes
// });
// export default router;
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
在 main.js
调整挂载方式
import Vue from "vue";
import { createApp } from "vue";
import App from "@/App.vue";
import router from "@/router";
const app = createApp(App);
app.use(router); // 挂载 router
app.mount("#app");
2
3
4
5
6
7
8
9
# 4. vuex 升级到 4.x
package.json
"dependencies": {
- "vuex": "^3.6.0",
+ "vuex": "^4.0.2",
...
}
2
3
4
5
src/store/index.js
// 4.x 版本
import { createStore as _createStore } from "vuex";
function createStore(router) {
return _createStore({
state: {
get route() {
return router.currentRoute.value;
},
// ...
},
// ...
});
}
const store = createStore(router);
export default store;
// 3.x 版本
// import Vue from "vue";
// import Vuex from "vuex";
// Vue.use(Vuex);
// const store = new Vuex.Store({
// state: {},
// // ...
// });
// export default store;
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
main.js
import Vue from "vue";
import { createApp } from "vue";
import App from "@/App.vue";
import router from "@/router";
import store from "@/store";
const app = createApp(App);
app.use(router);
app.use(store); // 挂载 store
app.mount("#app");
2
3
4
5
6
7
8
9
10
11
# 5. Element-UI 升级为 Element-Plus
package.json
"dependencies": {
- "element-ui": "^2.14.0",
+ "element-plus": "^2.0.1"
...
}
2
3
4
5
vue.config.js
module.exports = {
// ...
chainWebpack(config) {
config.resolve.extensions
.add([".mjs"])
.clear();
config.module // fixes https://github.com/graphql/graphql-js/issues/1272
.rule("mjs$")
.test(/\.mjs$/)
.include
.add(/node_modules/)
.end()
.type("javascript/auto");
}
// ...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
main.js
import { createApp } from "vue";
import ElementPlus from "element-plus"; // add
import zhCn from "element-plus/lib/locale/lang/zh-cn"; // add
import "element-plus/dist/index.css"; // add
import App from "@/App.vue";
import store from "@/store";
import router from "@/router";
const app = createApp(App);
app.use(store);
app.use(router);
// element 中文包
app.use(ElementPlus, { locale: zhCn });
app.mount("#app");
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 6. @element-plus/icons-vue
Element-Plus 把 icon 被抽离为一个 icon 库。
package.json
"dependencies": {
+ "@element-plus/icons-vue": "^1.0.0"
...
}
2
3
4
main.js
引入
import { createApp } from "vue";
import ElementPlus from "element-plus";
import zhCn from "element-plus/lib/locale/lang/zh-cn";
import * as ElIcons from "@element-plus/icons-vue"; // add
import App from "@/App.vue";
import store from "@/store";
import router from "@/router";
const app = createApp(App);
app.use(store);
app.use(router);
app.use(ElementPlus, { locale: zhCn });
// 挂载所有的 icon
Object.keys(ElIcons).forEach(key => {
app.component(key, ElIcons[key]);
});
app.mount("#app");
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
使用
<template>
<div>
<!-- 旧用法 -->
<span class="el-icon-search"></span>
<!-- 新用法 -->
<el-icon>
<Search />
</el-icon>
<div>
</template>
2
3
4
5
6
7
8
9
10
# 7. main.js
所有依赖更新后的 main.js
import { createApp } from "vue";
import ElementPlus from "element-plus";
import * as ElIcons from "@element-plus/icons-vue";
import "element-plus/dist/index.css";
import zhCn from "element-plus/lib/locale/lang/zh-cn";
import store from "@/store";
import router from "@/router";
import App from "@/App.vue";
import { message } from "@/utils/resetMessage";
const app = createApp(App);
app.use(store);
app.use(router);
// 引入 element 中文包
app.use(ElementPlus, { locale: zhCn });
// 挂载所有的 icon
Object.keys(ElIcons).forEach(key => {
app.component(key, ElIcons[key]);
});
// 挂载全局变量
app.config.globalProperties.$message = message;
app.mount("#app");
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
# 兼容问题
将上面所有的依赖升级完之后,重跑项目,会发现还是没办法成功启动,因为 vue3 有很多特性已经不兼容,需要把这些不兼容的特性都处理掉才能正常运行。调整前可以看官方文档,先按文档的顺序一个个调整 特性参考,调整完后再跑项目,根据提示再调整
TIP
在调整中发现,warning 虽然不会阻止项目运行,但会出现一些意想不到的问题,所以解决完 error 后最好也消灭掉所有警告。
下面记录几个调整比较多的几个点
# slot 调整
通过 slot
调整为 v-slot[slotName]
或简写为 #[slotName]
// 旧
<auto-column slot="autoColumn" />
// 新
<auto-column v-slot:autoColumn />
2
3
4
动态绑定 slot
<template v-for="item in list"
v-slot:[regionItem.label]>
<!-- ... -->
</template>
2
3
4
除了自定义组件,所有的
Element-Plus
的 slot 也需要调整
# .sync 替换为v-model
v-bind.sync
被替换为带参数的 v-model
,vue3 支持多个 v-model
<auto-column v-model:visible="visible" v-model:label="label" />
除了自定义组件,所有的
Element-Plus
的 .sync 也需要调整,像el-dialog
之类使用了 .sync 控制显示。
# 删除 $set、$delete 使用
vue3 使用 Proxy
实现响应式,对象属性可响应,不需要使用 $set
,可直接删除 $set
使用。
没有 $set
也就不需要 $delete
了,$delete
使用也需要去掉。
# 删除 $listeners、.native使用
$attrs
现在包含了所有传递给组件的 attribute,包括class
和style
。如果有 $attrs,就可以删掉$listeners
了.native
已经不需要,vue3 会自动将方法等添加到根元素
# filters已不支持
从 Vue 3.0
开始,过滤器已移除,且不再支持。需要将所有 filter
调整为 method 方法,或者 computed
。
# h 函数引入方式调整
在 Vue 3 中,所有的函数式组件都是用普通函数创建的。
// h 函数全局导入,不是在 render 函数中隐式提供
import { h, resolveComponent } from 'vue'
const DynamicFromItem = (props, context) => {
return h(
// 需要使用 resolveComponent 才能正确解析 element 组件
resolveComponent("el-form-item"),
{
class: [{
"line-form-item": isLineForm(formItemInfo),
"normal-item": isNormalItem(formItemInfo)
}],
label: formItemInfo.label,
prop: `${this.formItemProp}.value`
},
{
label: () => {
return scopedSlotsLabel;
},
default: (props) => {
const arr = [
...formItemRender
];
return arr;
}
});
};
export default DynamicFromItem;
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
# v-for 的 key
vue3 里 v-for
不需要再手动设置唯一 key 了。当然设置了也不会报错,但如果是组件多次复用的情况下,可能会出现视图无法更新的情况,所以最好还是把 v-for
里的 key 删掉。
# router-view与keep-alive
router-view
和 keep-alive
结合的写法改变了。
旧写法
<template>
<keep-alive>
<router-view />
</keep-alive>
</template>
2
3
4
5
6
新写法
<template>
<router-view v-slot="{ Component }">
<keep-alive>
<component :is="Component" />
</keep-alive>
</router-view>
</template>
2
3
4
5
6
7
# mixins里的route导航守卫会被忽略
如果在 mixins
里面写了 router
的导航守卫,会被忽略,需要移到主文件里面。
# element 组件
element组件的调整是个体力活,但几乎都是上面同类的几个问题,只要过一遍文档,根据文档的使用做全局替换即可。