去年Q3,我们开始把项目从 Vue 2 迁移到 Vue 3。原以为一个月就能搞定,结果花了三个月。
这篇文章记录我们踩过的坑,希望能帮到正在迁移的你。
为什么要迁移
Vue 2 官方已经停止维护了,不迁移不行:
- 安全漏洞不会修了
- 新的库都不支持 Vue 2
- 招人也不好招(谁还想写 Vue 2)
而且 Vue 3 确实有优势:
- 性能更好
- TypeScript 支持更好
- Composition API 更灵活
迁移策略
我们没有一次性全部迁移,而是分阶段:
阶段1:评估影响范围
先跑一遍 @vue/compat(Vue 2 兼容模式),看看哪些代码会报错。
npm install @vue/compat 然后配置 webpack:
module.exports = { resolve: { alias: { vue: '@vue/compat' } } }跑一遍项目,控制台会显示所有不兼容的 API。
我们项目有 200+ 个警告。
阶段2:先修简单的
有些改动很简单,比如:
$on、$off、$once被移除了 → 改用 mitt$children被移除了 → 改用 reffilter被移除了 → 改用 computed 或方法
这些改动风险低,优先处理。
阶段3:重点改造核心组件
有些组件用了很多 Vue 2 的特性,需要重写:
- 大量用了
this - 用了 mixins
- 用了全局过滤器
这些组件改成 Composition API,逻辑更清晰。
阶段4:升级第三方库
很多库也要升级:
- Vue Router 3 → 4
- Vuex 3 → 4(后来换成了 Pinia)
- Element UI → Element Plus
这是最麻烦的部分。
踩过的坑
坑1:v-model 的变化
Vue 2 里,v-model 绑定的是 value prop 和 input 事件。
Vue 3 里,v-model 绑定的是 modelValue prop 和 update:modelValue 事件。
我们有个组件:
// Vue 2 props: ['value'], methods: { handleChange(val) { this.$emit('input', val); } }要改成:
// Vue 3 props: ['modelValue'], methods: { handleChange(val) { this.$emit('update:modelValue', val); } }如果不改,v-model 就失效了。
坑2:事件总线不能用了
Vue 2 里,我们用 $on、$emit 做事件总线:
// EventBus.js export default new Vue(); // A组件 EventBus.$emit('some-event', data); // B组件 EventBus.$on('some-event', callback);Vue 3 移除了 $on、$off,事件总线不能用了。
我们改用 mitt:
import mitt from 'mitt'; export const emitter = mitt(); // A组件 emitter.emit('some-event', data); // B组件 emitter.on('some-event', callback);全局搜索 $on、$emit,改了100多处。
坑3:Vuex 要重写
Vuex 4 的 API 变化不大,但 TypeScript 支持还是很差。
我们直接换成了 Pinia:
// Vuex store.dispatch('user/login', payload); // Pinia userStore.login(payload);Pinia 的好处:
- 不用写 mutations
- TypeScript 支持完美
- 模块自动注册
但迁移成本也不小,我们花了一周时间。
坑4:插槽语法变了
Vue 2 里:
<template slot="header" slot-scope="{ data }"> {{ data.title }} </template>Vue 3 里:
<template #header="{ data }"> {{ data.title }} </template>还好有兼容模式,但迁移后都改成了新语法。
坑5:全局 API 改了
Vue 2 里,很多东西挂在 Vue 上:
import Vue from 'vue'; Vue.prototype.$http = axios; Vue.use(VueRouter); Vue.component('MyComponent', MyComponent);Vue 3 里,要用 app 实例:
import { createApp } from 'vue'; const app = createApp(App); app.config.globalProperties.$http = axios; app.use(router); app.component('MyComponent', MyComponent);所有插件初始化的代码都要改。
坑6:响应式 API 的坑
Vue 3 的 reactive 不能直接赋值:
let state = reactive({ count: 0 }); // ❌ 这样不行,会丢失响应式 state = { count: 1 }; // ✅ 要这样 state.count = 1;我们有个地方直接赋值了,结果数据不更新,debug了半天。
坑7:生命周期钩子变了
Options API 里,beforeDestroy 和 destroyed 改名了:
// Vue 2 beforeDestroy() {}, destroyed() {} // Vue 3 beforeUnmount() {}, unmounted() {}Composition API 里,要加 on 前缀:
import { onMounted, onUnmounted } from 'vue'; onMounted(() => {}); onUnmounted(() => {});Element Plus 的坑
从 Element UI 到 Element Plus,也踩了不少坑:
1. 图标要单独导入
// Element UI <el-button icon="el-icon-search"></el-button> // Element Plus import { Search } from '@element-plus/icons-vue'; <el-button :icon="Search"></el-button>所有图标都要手动导入,很麻烦。
2. 表单校验 API 变了
// Element UI this.$refs.form.validate(valid => {}); // Element Plus const formRef = ref(); formRef.value.validate(valid => {});3. 样式变量名变了
Element UI 的 CSS 变量是 --el-color-primary,Element Plus 是 --el-color-primary。
等等,好像没变?但有些确实变了,比如 --color-primary → --el-color-primary。
迁移工具
有些工具能帮上忙:
1. @vue/compat
兼容模式,让 Vue 2 的代码在 Vue 3 里跑起来,同时给出警告。
2. gogocode-plugin-vue
自动转换代码:
npx gogocode -s ./src -t gogocode-plugin-vue -o ./src-vue3但转换率只有60%左右,还是要手动改。
3. eslint-plugin-vue
配置 Vue 3 的规则,自动检查不兼容的写法。
迁移后的效果
| 指标 | Vue 2 | Vue 3 |
|---|---|---|
| 首屏加载 | 2.5s | 1.8s |
| 打包体积 | 1.2MB | 900KB |
| 渲染性能 | - | 提升30% |
值得迁移。
给正在迁移的人的建议
- 用兼容模式:先跑起来,再慢慢改
- 分模块迁移:别想一次性全改完
- 写好测试:迁移过程中容易出bug
- 先改核心组件:高频使用的优先改
- 考虑换 Pinia:比 Vuex 好用太多
- 留出足够时间:比你预期的要久
迁移很累,但值得。
参考资料:Vue 3 迁移指南
评论区