前阵子把一个维护了三年的Vue2老项目整体迁移到Vue3折腾了两周踩了一堆文档里没写明白的坑。这篇文章就是那段时间沉淀下来的Vue2转Vue3速查表直接做成了可以对照检查的清单。标题里的“迁移”两个字看起来很轻但真落起来牵扯到模板语法、全局API、路由、状态管理、构建工具、第三方组件库六个维度每一项都有不是你想象中那么平滑的变化。适合正在做版本升级、或者准备把老项目迁移到Vue3的团队也适合刚学Vue3想了解Vue2差异的同学。先说一个总判断如果只是写业务组件语法层面改动其实不大真正让人头疼的是那些藏在全局用法里的东西比如事件总线、过滤器、$set、插槽和作用域插槽的字段变化这些东西一个一个排查起来非常费时间。下面按迁移顺序把关键点过一遍。1. 迁移前先摸清家底版本差异与升级路径1.1 Vue2和Vue3的底层差异决定了改造量很多人在迁移前只盯着API改名其实真正影响代码行为的是响应式机制的重写。Vue2用的是Object.defineProperty对对象进行递归劫持所以新增属性、删除属性、通过下标改数组这些操作天然响应不了项目里到处是Vue.set和重新赋值数组的写法。Vue3换成了ES6的Proxy代理的是整个对象新增和删除属性都能被拦截于是Vue.set、Vue.delete这些方法整个消失了。听起来省事但新的坑也出现了reactive返回的proxy对象不能直接解构解构出来的变量会丢失响应式ref在模板里会自动解包可你在函数里取ref的值必须写.value很多老手都在这上面栽过跟头。实例创建方式也变了。Vue2是new Vue({render: h h(App)})然后挂到根节点Vue3改成了createApp(App).mount(#app)。这个变化直接影响了全局配置的写法Vue.use变成app.useVue.component变成app.componentVue.prototype.$http变成了app.config.globalProperties.$http。如果你的项目以前在main.js里注册了大量全局组件和指令迁移第一步就是把它们从构造函数的静态方法改成app实例方法上调用。响应式、实例化、全局API三块是迁移的地基不先理清后面全是冤枉路。1.2 整体重写还是渐进式迁移怎么选迁移方案一般分三种。项目规模小、组件数量在几十个以内依赖库也不复杂可以直接清空重写把main.js、router、store全部用Vue3规范重来业务组件逐个替换语法这种方式两三天就能跑通。项目超过两三百个组件里面还混着一堆老插件、全局混入、指令直接重写风险大这时候可以用vue/compat也就是Vue3提供的兼容构建版本。它能在不修改业务代码的前提下先跑起来同时会在控制台打印不兼容的API警告你可以按警告逐项修复最后再把compat模式关掉。不过要说明compat构建只是工具不是终点它只支持Vue 2行为的一小部分filter、$on这类彻底移除的API它也是不支持或者只能通过额外兼容插件处理。还有一种渐进式路径比较极端旧页面继续用Vue2新页面用Vue3通过微前端框架来组装。我一般不太推荐如果你真的需要微前端说明项目复杂度已经超过了版本迁移本身此时考虑的不光是Vue2转Vue3而是整体架构拆分。选型的时候建议先把依赖清单拉出来。UI组件库是最关键的Element UI不支持Vue3必须换成Element Plus或另外选型Ant Design Vue 1.x需要升到3.xVant需要升到4.x弹窗、表单、表格这些核心组件升级之后API名可能全变了。还有混用mixin深度较大的组件建议在正式迁移之前跑一遍eslint-plugin-vue的vue/no-deprecated-xxx规则能自动标出已经废弃的用法。我迁移前就是靠eslint先扫了一遍再结合全局搜索过滤掉Vue.filter、this.$set、this.$on三个高频写法工作量心里立刻就有数了。2. 模板和组件层的高频改动v-model、插槽与样式2.1 v-model彻底改版自定义v-model的新写法v-model是每个项目都用得最多的指令Vue2里它默认依赖value属性和input事件组件库里的很多组件为了配合v-model都要显式声明model:{prop:value,event:input}。Vue3把这一套统一定义成了modelValue和update:modelValue同时允许一个组件上写多个v-model。比如一个日期选择加金额输入的组合组件以前要分别写v-model和v-model:date这种用.sync修饰符才能实现的功能现在可以直接PriceDatePicker v-model:priceform.price v-model:dateform.date /对应的组件内部props: { price: Number, date: String }, emits: [update:price, update:date]跟v-model配套的.sync修饰符在Vue3里已经移除统一合并到v-model的带参数用法里。还要注意v-model的修饰符也变了比如你想实现一个“自动去掉首尾空格”的自定义v-model需要在子组件里接收modelModifiers或者priceModifiers这样的属性旧写法完全不同。迁移时的搜索代价值得关注value、input这种组合要改成:modelValue update:modelValue:foo.sync改成v-model:foo。2.2 $on/$off、$listeners、$scopedSlots消失后怎么办事件总线是Vue2项目里最常见的设计之一。两个非父子组件需要通信就在created里$onbeforeDestroy里$off再调一次this.$emit。Vue3把$on、$off、$once这三个实例方法全移除了原因很简单全局事件总线在组件销毁时很容易漏解绑埋下一堆内存泄漏隐患。但现在要迁移就得找替代方案我实测下来最顺手的替代是mitt它只有零头大小API还是on/off/emit改造成本极低。改法就是在main.js里创建一个全局mitt实例然后通过provide/inject或者globalProperties暴露给组件使用。$listeners合并到$attrs是另一个隐蔽变化。Vue2里组件上自定义事件都会收集到$listeners里父组件往组件根节点传递的class、style等非prop属性放在$attrs里。Vue3不再区分两者所有非prop的属性和事件统一进$attrs。好处是传属性更直观坏处是如果你以前自定义了inheritAttrs:false并手动处理$listeners来透传事件现在代码可能要改成从$attrs里取事件。插槽同理旧代码里的this.$scopedSlots作用域插槽对象在Vue3里被合并成$slots函数写法也变了比如旧代码this.$scopedSlots.header({ data: item })要改成slots.header?.({ data: item })2.3 过滤器移除和样式穿透写法更新过滤器这个功能在Vue3里彻底删掉了官方建议用普通函数或者computed代替。如果项目里有很多全局过滤器比如格式化日期、金额迁移时不要直接把Vue.filter改成app.config.globalProperties然后接着用因为模板语法里的{{ price | formatMoney }}在Vue3编译阶段就会直接报错。我的做法是把过滤器改成独立工具函数在需要的组件里import进来模板里改成{{ formatMoney(price) }}虽然模板稍微长了点但类型和测试都更友好。样式穿透也改了。Vue2里要修改子组件内部样式常规写法是/deep/或者升级到Vue3之后这两种写法已经删除统一用:deep()。比如.parent :deep(.child-btn) { color: red; }全局样式文件里如果还有::v-deep这种老写法迁移过程中不会直接报错但Vue3官方已经不推荐而且在新版编译器里可能会被当成普通伪类导致样式选择器失效。这个属于细枝末节但在大盘迁移时特别容易漏尤其是引用了Element Plus这类组件库后很多局部样式的优先级和Vue2时代完全不同建议把全站的样式穿透写法统一扫一遍。3. 路由和状态管理的迁移清单3.1 Vue Router从3到4初始化方式变了路由迁移是“看着简单换起来全是坑”的类型。Vue Router 3配合Vue2使用初始化是new VueRouter({routes, mode:history})Vue Router 4则改成createRouter({history: createWebHistory(), routes})。如果项目配置了base路径比如部署在/admin目录下Vue Router 3里写base:/adminVue Router 4里要写进createWebHistory(/admin)。组件内使用方式也有变化。选项式API里this.$router和this.$route仍然保留所以迁移的初期组件里不需要大改但如果你已经把组件改成组合式API就要用useRouter()和useRoute()这两个函数。router-link组件的属性变化比较坑比如tag属性用来渲染成其他标签Vue Router 4里移除了必须用v-slot自定义渲染exact-active-class还在但exact属性没了Vue Router 4默认就是精确匹配。通配符路由变化必须重点提醒。Vue2里经常写{path:*, component: NotFound}作为404页面这个写法在Vue Router 4里会启动失败必须改成{path: /:pathMatch(.*)*, component: NotFound}。如果之前还用过正则路由也要按新语法调整这部分路由参数校验非常严格稍不注意启动就白屏。3.2 Vuex、Pinia状态管理迁移的取舍Vuex从3升到4API外形变化不大但创建方式从new Vuex.Store变成了createStore组件内从this.$store改成useStore()。如果你的项目之前用了模块化结构通过mapState、mapGetters、mapMutations辅助函数来使用这些map辅助函数在Vuex4里依旧能用这点让很多老项目松了一口气。但要注意在组合式API的setup函数里map辅助函数不能直接使用this需要先用useStore拿到store实例再配合computed去取具体状态。如果你不是被Vuex绑定得很死我更建议趁着迁移直接引入Pinia。它的开发体验比Vuex好太多没有mutation这一层直接修改state即可store的定义像写一个setup函数类型推断完整还天然支持模块拆分不用再区分namespace。迁移成本也不算高原来在Vuex的actions里做异步逻辑的直接搬到Pinia的actions里只是把this.$store.commit改成调用store自身的方法。要注意的是Pinia和Vuex的state对响应式的处理细节不同比如Pinia里ref、reactive类型数据在store之间共享时最好用storeToRefs解构单纯解构会丢失响应式。3.3 动态路由和路由参数权限菜单迁移的常见坑涉及权限系统的项目大多会在登录后根据角色动态生成菜单和路由。Vue Router 3时代很多人用router.addRoutes()批量添加Vue Router 4把addRoutes移除了只保留addRoute单条添加动态路由的典型写法变成const route { path: /user/${role}, name: UserPage, component: () import(/views/UserPage.vue) } router.addRoute(route)还要注意如果同一个name的路由被重复添加Vue Router4会直接抛异常提示duplicate开发环境表现更明显Vue Router3里同名路由只是静默覆盖。所以动态添加之前要先判断router.hasRoute(name)有的话用router.removeRoute(name)移除再添加或者直接复用已有的name。路由参数这块在Vue3组合式API里最常见的错误是直接解构useRoute()的结果比如const { id } useRoute()一旦路由从/id/1切到/id/2组件不会重新渲染因为解构出来的id不是响应式的。正确做法是computed(() route.params.id)然后watch这个computed或者直接用route.params.id并配合key变化来强制组件重渲染。这个坑几乎百分之百会踩提前写进你们的编码规范里能少出很多bug。4. 工具链与部署适配Vite、Electron、前后端分离4.1 vue安装与脚手架选择直接上Vite迁移过程中最容易被忽略的是构建工具。旧项目如果是Vue CLI创建的第一反应是先升级vue-cli-service到5.x这个版本确实能支持Vue3和webpack 5但是我实测下来的体验并不好webpack 5加Vue3之后首次编译速度明显下滑热更新也有时候会卡。既然都升到Vue3了不如一步到位迁到Vite。新项目的创建直接用npm create vuelatest会生成完整的Vite Vue3 JS/TS模板vue.config.js要改造成vite.config.js环境变量的读取从process.env.VUE_APP_XXX变成import.meta.env.VITE_XXX代理配置从devServer.proxy挪到server.proxy。这里有一个容易忽略的点vue安装依赖的时候旧项目的node_modules里可能还残留着vue-template-compilerVue3需要的是vue/compiler-sfc必须卸载前者否则构建时会报模板编译版本冲突。IDE插件也建议换一下。Vue2时代主流是veturVue3项目更建议用Volar它能正确识别defineProps、defineEmits这些宏模板里的类型提示也会好很多。还有vue devtools插件Vue3需要6.x以上版本如果你浏览器里装的还是老的Vue2 devtools扩展打开新项目会发现面板不显示先升级浏览器插件再排查问题能省不少事。4.2 Electron打包Vue项目时碰到的问题很多桌面端项目是用electron打包vue项目这类项目在做Vue2转Vue3时核心难点不在Vue本身而是Electron主进程和渲染进程之间的通信方式。Electron的ipcMain和ipcRenderer跟Vue没有任何关系它们是基于Chromium的进程间通信API所以Vue2转Vue3并不会直接改变IPC的调用方式。但如果你之前把ipcRenderer直接挂在Vue.prototype.$ipc上全局用迁移到Vue3就要改成app.config.globalProperties.$ipc或者改成在preload脚本里通过contextBridge暴露一个invoke方法渲染层通过provide/inject注入。我建议后者因为Electron的安全设置里nodeIntegration通常已经关掉了渲染进程里不能再直接require(electron)把通信封装在preload里对主进程和渲染进程都更安全。Vue3的组件拆分会更彻底意味着渲染进程里的页面逻辑更重如果出现页面白屏先看主进程webPreferences里的contextIsolation配置和preload路径再看构建产物的路径是否指向dist目录。Electron里因为使用了history模式还可能导致加载不到资源最简单的方式是改成hash模式或者把publicPath调成相对路径。4.3 Spring Boot前后端分离项目怎么配合前后端分离的Spring Boot项目在Vue2转Vue3时后端基本不用动但有几个点要提前约定好。开发环境跨域Vue2时代在vue.config.js的devServer.proxy里配置代理Vite里改成vite.config.js的server.proxy语法上会有细微差异比如以/api前缀为例server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: path path.replace(/^\/api/, ) } } }生产环境一般会把前端构建后的dist目录拷贝到Spring Boot的static目录下或者单独部署。如果你的后端用spring boot vue前后端分离架构前端路由用的history模式那么刷新页面时后端必须把所有未知路径转发到index.html否则刷新就404。Spring Boot里加一个简单的Controller或者Filter把所有非静态资源、非api的路径forward到index.html即可。还有一个容易被忽略的点如果后端接口统一有前缀比如/apiVue3项目里的axios baseURL要跟着后端约定走迁移时不要只盯着前端代码前后端联调接口路径也最好趁这次一起梳理清楚。5. 速查对照表与排查经验5.1 高频改动对照表这一节就是把上面所有改动浓缩成一张可以直接抄作业的速查表迁移时对照着改就行。需要特别说明的是这份表是基于大量项目的共性总结具体到某个项目还是要以你自己依赖库的实际文档为准。场景Vue2老写法Vue3新写法创建应用new Vue({…}).$mount(#app)createApp({…}).mount(#app)全局属性Vue.prototype.$httpapp.config.globalProperties.$http全局注册组件Vue.component(x, X)app.component(x, X)v-model默认value inputmodelValue update:modelValue参数化双向绑定:foo.syncv-model:foo自定义事件总线this.$on / this.$offmitt 或 provide/inject过滤器{{ price | fmt }}{{ fmt(price) }}样式穿透/deep/ 或 ::v-deep:deep()生命周期销毁beforeDestroy / destroyedbeforeUnmount / unmounted异步组件() import(...)defineAsyncComponent(() import(...))路由初始化new VueRouter({mode:history})createRouter({history: createWebHistory()})404通配路由path:*path: /:pathMatch(.)动态添加路由router.addRoutes([…])router.addRoute(route)Vuex初始化new Vuex.StorecreateStore组合式取Storethis.$storeuseStore()全局属性替换$attrs $listeners$attrs已合并插槽对象this.$scopedSlots$slots表格里的这些改动粗略估算能覆盖一次Vue2转Vue3迁移中70%的报错。剩下30%主要是第三方组件库的API变化这个只能按你项目实际使用的组件库去单独对照。5.2 常见问题与排查技巧实录把这次迁移过程中遇到的典型问题按出现频率整理成清单按图索骥能少走很多弯路。第一个是事件总线失联。表现是页面间通信突然全不生效控制台还会看到$on is not a function。原因就是$on被移除需要按前面说的方法换成mitt或emitter。这里有一个经验迁移前先全局搜索$on、$off、$once、EventBus这几个关键词把所有用到的地方集中改完再启动项目比报一个改一个好得多。第二个是组件改造后莫名其妙的白屏。最常见原因是异步组件没有包defineAsyncComponent。Vue2里component: () import(...)是合法的Vue3里这种写法会报警告而且组件内容不渲染。另一个常见原因是使用了不存在的filter语法Vue3编译阶段直接报错把模板里的管道符改成函数调用就行。第三个是v-if和v-for的优先级变化。Vue2里v-for的优先级高于v-if同一个元素上同时使用两者时v-if每次循环都会执行Vue3里v-if优先级更高这会导致原本依赖v-if来过滤列表的逻辑行为完全改变而且不会报错属于最难排查的静默错误。迁移时建议把同元素上的v-if和v-for拆开用template包一层或者在computed里先过滤。第四个是响应式丢失。主要在reactive对象解构或者直接整体替换数组时出现。如果你发现视图没更新先检查是不是用了let state reactive({list:[]})然后state getData()这种整体替换写法应该改成Object.assign(state, getData())或者干脆用ref保存整个响应式数据。第五个是CSS样式错乱。很多Vue2迁移的项目发现Element组件样式不对、下拉框位置异常主要原因是样式穿透语法还没换以及全局样式里引用了旧的变量名。建议先扫描所有/deep/、和::v-deep统一替换成:deep()。第六个是Spring Boot部署后刷新404。这个前面提过history模式如果后端没有处理fallback刷新非根路径就会404。验证方法很简单本地构建产物起一个nginx静态服务直接刷新子路由页面如果404就说明后端要加转发。第七个是Electron环境里请求CORS失败。如果渲染进程里直接访问了外部接口而主进程WebContents没做跨域处理迁移后因为代理配置变化更容易触发。开发环境下尽量用vite的server.proxy。最后说一个团队协作上的经验。迁移时不要让所有人同时开工先让一个核心成员把基础骨架、路由、Store、全局注册这些基础设施完成再让业务开发人员按模块分批改造。每改完一个模块就在验收环境完整跑一遍相关页面不要等全部改完再统一测试。我实际遇到的情况是既然骨架是按Vue3搭的早期改完的模块反而成了后面同事的参考模板照着写比查文档效率高得多。我自己做下来的感觉是Vue2转Vue3的难点不是某个API不会写而是旧习惯改不过来。响应式、事件总线、过滤器、插槽这些概念在项目里相互缠绕光靠看迁移文档很难一次解决最有效的方式是先跑通一个最小可用的迁移模板再拿真实业务组件往里填。这份速查表就是我在这个过程中沉淀出来的后面每次遇到迁移相关问题我都会先打开这张表对照一遍基本能锁定90%的问题范围。
