1. 项目概述与升级动因
最近在维护一个老项目,技术栈是Vue 2.x + Webpack 4,随着项目迭代和团队新成员的加入,Vue 2.x 的一些局限性开始显现,比如 Composition API 的缺失让复杂逻辑复用变得困难,TypeScript 的支持也不够原生和友好。更重要的是,Vue 2 将在 2024 年底进入生命周期结束阶段,这意味着官方将不再提供新功能和安全更新。因此,将项目从 Vue 2 平稳升级到 Vue 3,从一个“技术债”变成了一个必须提上日程的“技术投资”。这个升级过程远不止是改个版本号那么简单,它涉及到核心 API 的变更、生态库的适配、构建工具的升级以及团队开发习惯的转变。我花了大约两周时间,完成了从评估、实施到验证的全过程,过程中积累了一份非常详细的“修改清单”。这份清单不是简单的命令罗列,而是包含了每个改动背后的原因、可能遇到的坑以及具体的解决方案,希望能给面临同样升级任务的你提供一个清晰的路线图。
2. 升级前准备与评估
在动手写第一行代码之前,充分的准备工作是决定升级成败的关键。盲目升级只会导致项目在某个环节卡住,进退两难。
2.1 环境与依赖全面审计
首先,你需要对现有项目进行一次彻底的“体检”。打开package.json,这是你的起点。除了记录 Vue 版本(通常是vue@^2.6.14),更要重点关注与 Vue 强相关的生态库。使用命令npm list vue vue-router vuex或yarn why vue vue-router vuex来查看它们确切的版本和依赖关系。
接下来,制作一个依赖兼容性矩阵表。Vue 3 的核心变化导致许多流行库需要升级到特定版本才能兼容。你需要逐一核对。以下是我在升级时整理的核心清单,你的项目可能还涉及其他库(如 UI 库、图表库等),需要单独调研。
| 库名称 | Vue 2 典型版本 | Vue 3 兼容版本 | 升级关键点 |
|---|---|---|---|
| Vue | 2.6.x | 3.2.x 或更高 | 核心对象,必须升级 |
| Vue Router | 3.x | 4.x | API 基本一致,但创建方式、部分钩子名变更 |
| Vuex | 3.x | 4.x | API 基本一致,创建方式变更,须用createStore |
| Vue CLI / Vite | Vue CLI 4/5 | Vite 推荐 | 构建工具建议迁移至 Vite 以获得最佳体验 |
| Element UI | 2.x | Element Plus1.x 或更高 | 不是升级,是替换为全新的 Element Plus 库 |
| Vuetify | 2.x | 3.x | 有官方升级指南,但变动较大,需仔细评估 |
| Vue-i18n | 8.x | 9.x | API 有重大变化,需迁移 |
| Vue Test Utils | 1.x | 2.x | 测试 API 变化很大,测试用例需要重写或调整 |
注意:对于大型 UI 库(如 Element UI、Ant Design Vue),务必查阅其官方提供的 Vue 3 迁移指南或版本。它们通常不是简单升级,而是提供了一个全新的 Vue 3 兼容版本(如 Element Plus),这意味着你需要修改大量组件导入和部分 API 调用。
2.2 代码库健康度检查
依赖理清后,就要审视自己的代码了。Vue 3 移除或改变了部分 Vue 2 API,你的代码里可能藏着这些“地雷”。
- 使用官方迁移构建模式:Vue 官方提供了一个
@vue/compat包,它允许你在 Vue 3 环境中以“兼容模式”运行 Vue 2 代码,并会在控制台发出警告,指出哪些写法需要修改。这是最有效的发现工具。你可以先创建一个临时分支,安装@vue/compat并按照指南配置,然后运行项目,查看控制台输出的所有警告和错误,逐一记录。 - 重点扫描清单:
- 过滤器 (Filters):Vue 3 已移除。全局过滤器需要改用全局方法或计算属性;局部过滤器需改为组件内的方法或计算属性。
- 事件 API (
$on,$off,$once):已移除。依赖事件总线的代码需要重构,推荐使用mitt或tiny-emitter这类第三方库替代。 - 按键修饰符:
keyCode支持已移除。需要将类似v-on:keyup.13改为v-on:keyup.enter。 $children和$listeners:已移除。访问子组件推荐使用ref和$attrs。- 生命周期钩子:
destroyed应改为unmounted,beforeDestroy应改为beforeUnmount。
- 构建工具评估:如果你的项目使用 Vue CLI,升级到 Vue 3 后可以继续使用 Vue CLI(需升级到 v5),但更推荐借此机会迁移到Vite。Vite 的启动速度和热更新速度有数量级的提升,能极大改善开发体验。评估一下项目对 Webpack 特定插件或配置的依赖程度,规划迁移成本。
3. 分步升级实施清单
准备工作做完,手里有了一份“问题清单”,现在可以开始按步骤实施了。我建议在一个独立的功能分支上进行,并频繁提交,便于回滚。
3.1 第一步:更新 package.json 与依赖安装
这是最直接的一步,但需要小心依赖冲突。
- 修改版本号:在
package.json中,将vue的版本更新为^3.2.0或更高稳定版。同时,根据之前的兼容性矩阵,更新vue-router到^4.0.0,vuex到^4.0.0。 - 处理 UI 库:以 Element UI 为例,你需要卸载旧的,安装新的。
npm uninstall element-ui npm install element-plus # 同时,你可能需要安装按需导入的插件 npm install -D unplugin-vue-components unplugin-auto-import - 清理并安装:删除
node_modules和package-lock.json(或yarn.lock),然后运行npm install或yarn install重新安装所有依赖。这一步可能会报错,提示某些包不兼容,需要你根据错误信息进一步调整版本或寻找替代包。
3.2 第二步:修改入口文件与 Vue 实例创建
Vue 3 的初始化方式从“构造函数”变成了“工厂函数”,这是第一个需要适应的代码改动点。
Vue 2 的写法 (src/main.js):
import Vue from 'vue' import App from './App.vue' import router from './router' import store from './store' Vue.config.productionTip = false new Vue({ router, store, render: h => h(App) }).$mount('#app')Vue 3 的写法 (src/main.js):
import { createApp } from 'vue' // 注意是从 'vue' 导入 createApp import App from './App.vue' import router from './router' import store from './store' // 不再需要 Vue.config.productionTip const app = createApp(App) // 创建应用实例 // 使用 use 方法注册插件 app.use(router) app.use(store) // 挂载 app.mount('#app')关键变化解析:
createApp是一个函数,调用它返回一个应用实例app。- 全局配置(如
Vue.config.xxx)现在通过应用实例app.config进行设置。 - 插件(Router, Store, i18n等)不再自动注入,需要通过
app.use()显式安装。 - 全局组件、指令、混入 (mixin) 的注册也改为通过
app.component(),app.directive(),app.mixin()方法。
3.3 第三步:逐项解决 API 与语法变更
按照之前“代码健康检查”列出的清单,开始批量修改源代码。这是最耗时但也最核心的一步。
1. 过滤器迁移: 查找所有|管道符的使用。
- 全局过滤器:在
main.js中,改为注册全局方法或使用插件。// Vue 2: Vue.filter('currency', ...) // Vue 3: app.config.globalProperties.$filters = { currency(value) { /* ... */ } } // 模板中使用:{{ $filters.currency(price) }} - 局部过滤器:在组件选项中移除
filters属性,改为methods或computed。// Vue 2: filters: { currency(value) {...} } // Vue 3: methods: { currency(value) { /* ... */ } } // 模板中使用:{{ currency(price) }}
2. 事件总线重构: 如果项目使用了new Vue()作为事件总线,需要替换。
- 安装
mitt:npm install mitt - 创建一个事件总线工具文件(如
src/utils/eventBus.js):import mitt from 'mitt' const emitter = mitt() export default emitter - 在需要的地方导入并使用:
import emitter from '@/utils/eventBus' // 发送事件 emitter.emit('some-event', data) // 监听事件 emitter.on('some-event', (data) => { ... }) // 移除监听 emitter.off('some-event', handler)
3. 生命周期钩子重命名: 使用编辑器的全局搜索替换功能,将beforeDestroy替换为beforeUnmount,将destroyed替换为unmounted。注意选项式 API 和 Composition API 中名称一致。
4.$children和$listeners移除:
$children:如果需要访问子组件实例,应该使用ref。在父组件模板中给子组件添加ref="childRef",然后在脚本中通过this.$refs.childRef访问。$listeners:在 Vue 3 中,$attrs包含了传递给组件的所有属性和事件监听器(除了class和style)。如果你之前在组件内手动处理v-on="$listeners",现在需要改为v-bind="$attrs"(实际上,在 Vue 3 的组件中,未声明的属性和事件监听器会自动继承到根元素,除非设置inheritAttrs: false)。
3.4 第四步:Vue Router 与 Vuex 升级调整
这两个官方库的 API 在 Vue 3 中变化相对较小,但创建方式必须更新。
Vue Router 4:
- 创建方式从
new VueRouter()变为createRouter()。 - 模式定义从
mode: 'history'变为history: createWebHistory()。 router.push()的next参数在导航守卫中被移除,现在守卫函数返回false表示取消导航。
src/router/index.js修改示例:
import { createRouter, createWebHistory } from 'vue-router' // 注意导入 import routes from './routes' // 你的路由表 const router = createRouter({ history: createWebHistory(process.env.BASE_URL), // 替代 mode routes }) export default routerVuex 4:
- 创建方式从
new Vuex.Store()变为createStore()。 - 其他核心概念(state, getters, mutations, actions, modules)用法基本不变。
src/store/index.js修改示例:
import { createStore } from 'vuex' // 注意导入 export default createStore({ state: { ... }, mutations: { ... }, actions: { ... }, modules: { ... } })3.5 第五步:构建工具迁移(可选但推荐)
如果你决定从 Vue CLI (Webpack) 迁移到 Vite,这一步可以显著提升开发幸福感。
安装 Vite 及相关插件:
npm install -D vite @vitejs/plugin-vue # 如果使用 Vue Router 4 和 Vuex 4,它们已支持 Vite,无需额外插件 # 如果使用 Element Plus,安装对应的按需导入插件 npm install -D unplugin-vue-components unplugin-auto-import创建
vite.config.js:import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import { resolve } from 'path' import AutoImport from 'unplugin-auto-import/vite' import Components from 'unplugin-vue-components/vite' import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' export default defineConfig({ plugins: [ vue(), // Element Plus 按需导入 AutoImport({ resolvers: [ElementPlusResolver()], }), Components({ resolvers: [ElementPlusResolver()], }), ], resolve: { alias: { '@': resolve(__dirname, 'src') // 保持与 Webpack 相同的别名 } }, server: { port: 8080, // 指定开发服务器端口 open: true // 自动打开浏览器 } })修改
index.html与入口:- 将
public/index.html移动到项目根目录。 - 在
index.html中,删除所有关于%=的 Webpack 模板变量,并通过<script type="module" src="/src/main.js"></script>直接引入入口文件。 - 确保
src/main.js的导入路径正确。
- 将
更新
package.json脚本:{ "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview" } }处理环境变量:Vite 使用
import.meta.env替代process.env。你需要将代码中所有的process.env替换为import.meta.env,并且环境变量前缀从VUE_APP_改为VITE_。
4. 升级后验证与测试
代码修改完成后,绝不能直接部署。必须经过严格的验证。
4.1 基础功能冒烟测试
- 启动开发服务器:运行
npm run dev,确保项目能成功启动,没有白屏和明显的运行时错误。 - 核心流程走查:手动测试最关键的用户路径,例如:首页加载、用户登录、主要数据列表查看、表单提交、页面跳转等。确保基本交互正常。
- 浏览器控制台检查:打开开发者工具,查看 Console 和 Network 面板,确保没有未处理的警告和 404 请求(特别是静态资源路径在 Vite 下可能变化)。
4.2 单元测试与端到端测试修复
如果你的项目有测试用例(这非常棒!),现在它们大概率会全部失败。
- 单元测试 (如 Jest + Vue Test Utils):Vue Test Utils 从 v1 升级到 v2,API 有破坏性变更。最常见的是:
mount和shallowMount的返回值变了,访问 wrapper.vm 的方式可能不同。find和findAll的选择器语法可能更严格。- 需要更新测试工具配置以支持 Vue 3。你需要参照 Vue Test Utils v2 的迁移指南,逐个修复测试文件。这是一个细致活,但能确保你的组件逻辑在升级后依然正确。
- 端到端测试 (如 Cypress):通常影响较小,只要页面元素和交互流程没变,测试用例只需重新运行即可。但需注意如果组件类名或结构因 UI 库更换而改变,需要更新选择器。
4.3 性能与打包分析
- 构建分析:运行
npm run build,观察打包过程是否有错误或警告。对比升级前后的打包体积(查看dist文件夹大小,或使用rollup-plugin-visualizer生成分析报告)。由于 Vue 3 本身更轻量,以及 Vite 的优化,通常打包体积会有所减少。 - 运行时性能:在浏览器开发者工具的 Performance 面板中,记录关键操作(如页面切换、大数据列表渲染),与升级前进行粗略对比,确保没有明显的性能回退。
5. 常见问题与排查实录
在升级过程中,我遇到了不少“坑”,这里记录下最典型的几个及其解决方案。
5.1 第三方库控制台警告 “Component missing template or render function”
问题描述:启动项目后,控制台大量警告,提示某个组件缺少模板或渲染函数,但页面似乎又能正常显示一部分。根本原因:这是 UI 库组件按需导入配置不正确导致的。在 Vite 中,使用unplugin-vue-components自动导入组件时,该插件会在编译时动态解析并注册组件。如果配置有误或组件名写错,Vue 在运行时找不到对应的组件定义,就会抛出此警告。解决方案:
- 检查
vite.config.js中Components插件的resolvers配置是否正确指向了你使用的 UI 库(如ElementPlusResolver())。 - 确保在模板中使用的组件名与 UI 库导出的名称完全一致(注意大小写)。
- 对于某些无法被自动导入器识别的特殊组件,考虑在局部手动导入并注册。
- 可以暂时在
main.js中全局导入整个 UI 库以确认是否是按需导入的问题:app.use(ElementPlus)。如果警告消失,则问题肯定出在按需导入配置上。
5.2 项目中使用了 Vue.extend 定义的组件无法渲染
问题描述:一些使用Vue.extend({ ... })定义的传统组件在升级后渲染空白或报错。根本原因:Vue 3 中defineComponent是 TypeScript 友好且推荐的方式,但Vue.extend在多数情况下仍能工作。问题可能出在组件内部使用了已被移除的 Vue 2 API(如过滤器),或者混入 (mixin) 中存在兼容性问题。解决方案:
- 优先将
Vue.extend改为defineComponent,这是一个简单的替换,通常能解决大部分问题。// 之前 import Vue from 'vue' export default Vue.extend({ ... }) // 之后 import { defineComponent } from 'vue' export default defineComponent({ ... }) - 检查该组件及其混入的代码,确保没有使用过滤器、
$on/$off等已移除的 API。 - 如果组件逻辑复杂,考虑将其重构为 Composition API setup 函数,这能更好地利用 Vue 3 的新特性。
5.3 样式丢失或布局错乱
问题描述:升级 UI 库(如 Element UI -> Element Plus)后,页面样式变得混乱,组件间距、颜色、字体大小都不对。根本原因:UI 库的 CSS 样式名称、CSS 变量或默认样式可能发生了改变。此外,从 Webpack 迁移到 Vite 后,CSS 预处理器的全局变量、混入文件导入路径可能失效。解决方案:
- 检查全局样式:确保正确引入了新 UI 库的样式文件。对于 Element Plus,可能需要手动引入
index.css或在插件解析器中配置导入样式。// vite.config.js - Components 插件配置 Components({ resolvers: [ ElementPlusResolver({ importStyle: 'css', // 确保导入样式 }), ], }), - 核对自定义覆盖样式:你之前可能通过更高优先级的选择器覆盖了原 UI 库样式。新库的 CSS 类名可能已变,导致你的覆盖样式失效。需要打开浏览器检查器,找到目标元素的新类名,并更新你的自定义 CSS。
- 检查 CSS 预处理器配置:在
vite.config.js中,可能需要重新配置css.preprocessorOptions来引入全局的 SCSS/Less 变量文件。export default defineConfig({ css: { preprocessorOptions: { scss: { additionalData: `@import "@/styles/variables.scss";` // 全局变量 } } } })
5.4 路由跳转或状态管理相关错误
问题描述:点击路由链接无反应,或页面刷新后 Vuex 状态丢失。根本原因:
- 路由:Vue Router 4 的初始化方式或导航守卫用法有误。例如,在守卫中使用了已被移除的
next参数。 - 状态管理:在 Vue 3 的 setup 函数中,访问
this.$store的方式已改变。或者,在组合式函数中使用了错误的导入方式。解决方案:
- 路由:确保路由实例是通过
app.use(router)正确安装的。检查导航守卫,将next()调用改为返回true或undefined(表示通过),返回false表示取消,返回一个路由路径对象进行重定向。// Vue Router 4 导航守卫 router.beforeEach((to, from) => { // 返回 false 取消导航 // return false // 返回 undefined 或 true 继续 // return true // 重定向 // return { path: '/login' } }) - Vuex:在组合式 API 中,使用
useStore钩子来获取 store 实例。
在选项式 API 中,仍然可以通过import { useStore } from 'vuex' export default { setup() { const store = useStore() // 现在可以使用 store.state, store.commit, store.dispatch return {} } }this.$store访问,前提是 store 已通过app.use(store)正确安装。
整个升级过程就像给一架正在飞行的飞机更换引擎,需要缜密的计划、细致的操作和充分的测试。我的体会是,不要试图在一天内完成所有工作。建立一个清晰的清单,分模块、分步骤进行,每完成一个模块就进行验证。充分利用@vue/compat构建模式来发现潜在问题,它能在早期为你节省大量调试时间。最后,升级不仅是技术栈的更新,更是团队拥抱更现代、更高效开发模式的机会,尤其是 Composition API 的引入,为复杂逻辑的组织和复用打开了新的大门。在升级完成后,可以鼓励团队在新功能开发中尝试使用<script setup>语法和组合式函数,逐步享受 Vue 3 带来的开发体验提升。