《Vue2 升级到 Vue3 记录》


2022-05-19 上次更新时间:5/20/2022, 11:45:13 AM 0 javascript

# 背景

领导想在现在的项目中使用 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.jsonvue 更新到 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"
}

1
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
            }
          }
        }
      })
  }
}
1
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");
1
2
3
4
5
6

# 3. vue-router 升级到 4.x

package.json

"dependencies": {
-  "vue-router": "3.3.4",
+  "vue-router": "^4.0.12",
   ...
}
1
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;
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

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");
1
2
3
4
5
6
7
8
9

# 4. vuex 升级到 4.x

package.json

"dependencies": {
-  "vuex": "^3.6.0",
+  "vuex": "^4.0.2",
   ...
}
1
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;
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

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");
1
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"
   ...
}
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");
    }
    // ...
}
1
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");
1
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"
   ...
}
1
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");
1
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>
1
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");
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

# 兼容问题

将上面所有的依赖升级完之后,重跑项目,会发现还是没办法成功启动,因为 vue3 有很多特性已经不兼容,需要把这些不兼容的特性都处理掉才能正常运行。调整前可以看官方文档,先按文档的顺序一个个调整 特性参考,调整完后再跑项目,根据提示再调整

TIP

在调整中发现,warning 虽然不会阻止项目运行,但会出现一些意想不到的问题,所以解决完 error 后最好也消灭掉所有警告。

下面记录几个调整比较多的几个点

# slot 调整

通过 slot 调整为 v-slot[slotName] 或简写为 #[slotName]

// 旧
<auto-column slot="autoColumn" />
// 新
<auto-column  v-slot:autoColumn />
1
2
3
4

动态绑定 slot

<template v-for="item in list"
          v-slot:[regionItem.label]>
    <!-- ... -->
</template>
1
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" />
1

除了自定义组件,所有的 Element-Plus 的 .sync 也需要调整,像 el-dialog 之类使用了 .sync 控制显示。

# 删除 $set、$delete 使用

vue3 使用 Proxy 实现响应式,对象属性可响应,不需要使用 $set,可直接删除 $set 使用。

没有 $set 也就不需要 $delete 了,$delete使用也需要去掉。

# 删除 $listeners、.native使用

  • $attrs 现在包含了所有传递给组件的 attribute,包括 classstyle。如果有 $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;
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

# v-for 的 key

vue3 里 v-for 不需要再手动设置唯一 key 了。当然设置了也不会报错,但如果是组件多次复用的情况下,可能会出现视图无法更新的情况,所以最好还是把 v-for 里的 key 删掉。

# router-view与keep-alive

router-viewkeep-alive 结合的写法改变了。

旧写法

<template>
    <keep-alive>
        <router-view />
    </keep-alive>
</template>

1
2
3
4
5
6

新写法

<template>
    <router-view v-slot="{ Component }">
      <keep-alive>
        <component :is="Component" />
      </keep-alive>
    </router-view>
</template>
1
2
3
4
5
6
7

# mixins里的route导航守卫会被忽略

如果在 mixins 里面写了 router 的导航守卫,会被忽略,需要移到主文件里面。

# element 组件

element组件的调整是个体力活,但几乎都是上面同类的几个问题,只要过一遍文档,根据文档的使用做全局替换即可。

# 记录一些报错/警告

# (warning): Extraneous non-props attributes

# (warning): directive used on non-element node

# (warning): Non-function value for default slot

# (warning): prefer function slots for better

# (error): transfer valid prop path to form item

# (error): Cannot read property 'insertBefore' of null

上次更新时间: 5/20/2022, 11:45:13 AM