创建日期:2026-09-08 | 最近更新:2026-09-08 面向「真实 Vue2 项目要切 Vue3」。逐项给「没了什么 → 换成什么」;拿不准的以官方 迁移指南 为准。本文不伪造运行日志。
迁移深水区:Vue2 老项目逐项替换清单
一句话:语法差异只是表,真正让老项目「卡住」的是几类结构性替换——全局 API、
v-model/事件总线/过滤器这种「语言层 API 没了」、以及状态与路由的换库。这篇就是一张照着做就行的迁移 runbook。
1. 先从「跑起来」入手:换构建链
Vue2 老项目多是 webpack + vue-loader + Vue.use 全家桶。最快路径不是手改 webpack,而是换 Vite:
npm create vue@latest # 新骨架,把组件/逻辑逐步搬
# 或渐进:老工程保留,新页面用 Vue3 + Vite 独立入口,通过 iframe/微前端桥接
- 迁移期常采用「共存」:主工程仍 Vue2,新功能用 Vue3 微前端/独立子应用承载,避免一刀切大爆炸。
- Vue 官方提供
@vue/compat(迁移构建),让 Vue3 以兼容模式警告不兼容用法,适合大项目渐进——但它不是长期方案,目标仍是清掉不兼容点。
2. 语言层 API:没了 → 换成
过滤器(filters)
// Vue 2
{{ price | toYuan }} filters: { toYuan(v){ return (v/100).toFixed(2) } }
// Vue 3 —— 没有 filters 了
const toYuan = (v) => (v / 100).toFixed(2) // 全局 helper
{{ toYuan(price) }} // 模板里直接调函数
// 需要「全局统一格式化」时:挂 app.config.globalProperties.$filters = { toYuan }
推荐:转成模板内可用的函数(<script setup> 顶层函数 / import 的 helper / computed)。filters 最大的坑是「管道式书写爽,但断点与类型都难」,迁移正好戒掉。
this.delete
// Vue 2:加属性要这样,否则不响应
this.$set(this.obj, 'newKey', 1); this.$delete(this.obj, 'oldKey')
// Vue 3:直接写就行(Proxy 能侦测新增/删除,篇 3 讲原理)
obj.newKey = 1
delete obj.oldKey
事件总线(this.off)与非父子通信
// Vue 2
const bus = new Vue(); bus.$on('x', fn); bus.$emit('x')
// Vue 3 —— 内置总线没了,最小替代:mitt
import mitt from 'mitt'
export const bus = mitt() // bus.on / bus.emit / bus.off
// 更好的替代:provide/inject + 组合式函数(篇 1 §5);全局共享状态用 Pinia(§6)
v-model / .sync / .native / $listeners
| Vue 2 | Vue 3 |
|---|---|
<Child v-model="x">(内部是 value+input) | 内部是 modelValue + update:modelValue(组件里可用 defineModel) |
<Child :title.sync="t"> | v-model:title="t"(.sync 移除) |
@click.native="fn" | 移除——组件根节点原生事件默认透传(fallthrough) |
this.$listeners | 合并进 this.$attrs / useAttrs()(3.3+ 的 $attrs 自带 on* 监听器) |
| 事件值 | 回调参数可能包 $event |
深水点:Vue2 里给组件根元素「透传属性 + 监听器」要
.native+$listeners;Vue3 的规则变成「没被 props/emits 声明的,一律落到根节点」,不想落到根就用inheritAttrs: false+ 手动v-bind="$attrs"。
v-if / v-for 同元素
Vue 2 里 v-for 优先(同一元素能共存);Vue 3 里 v-if 优先,两者写一起等于 v-if 拿不到循环变量。迁移:把 v-for 提到 <template v-for> 外层,或干脆分成两步 computed。
3. 组件形态变化
多根(Fragment)与 attrs 落点
Vue 3 组件可以多根,代价是 $attrs 不再自动落到某个根——想透传就要显式绑:
<!-- Vue3:多个根,attrs 不会自动挂 -->
<template>
<header>…</header>
<main v-bind="$attrs">…</main> <!-- 主动选一个根接收透传 -->
</template>
异步组件
// Vue2
const Foo = () => import('./Foo.vue')
// Vue3 —— 要 defineAsyncComponent 包一下(还能配 loading/error)
import { defineAsyncComponent } from 'vue'
const Foo = defineAsyncComponent(() => import('./Foo.vue'))
配合 Suspense 可让整棵异步树统一 loading(§4)。
函数式组件
functional: true 的函数式组件在 Vue 3 里直接用普通组件代替(性能差异已无关紧要);必要场景可用仅含 render 的组件。
全局/实例 API 收编(插件作者尤其注意)
// Vue2 插件
export default { install(Vue){ Vue.component('X', X); Vue.prototype.$t = t } }
// Vue3 插件
export default { install(app){ app.component('X', X); app.config.globalProperties.$t = t } }
Vue.mixin/component/directive/use→ 全都在app实例上(app.mixin/app.component/...);- 多实例不再打架,但也意味着老代码里「全局
Vue.xxx」都要收到某个 app。
4. 新能力值得顺手用上
迁移不是「只删旧」,下面三个是 Vue3 白送、Vue2 要装插件的:
<Teleport>:把内容渲染到别处 DOM(弹层/全局提示)。Vue2 得用 portal-vue。<Suspense>:异步组件/异步 setup 的统一 fallback 骨架屏。<KeepAlive>语义 +onActivated/onDeactivated:和 Vue2 差不多,但能配组合式生命周期。
5. 老库逐项替换表
| 库 | Vue 2 时代 | Vue 3 时代 |
|---|---|---|
| 状态 | Vuex 3/4 | Pinia(官方推荐,无 mutations,setup store 与组合式无缝) |
| 路由 | vue-router 3 | vue-router 4+(createRouter/createWebHistory) |
| 请求 | axios(不变) | axios(不变);组合式里封装 useRequest(篇 5) |
| 拖拽/表格/UI 库 | element-ui(Vue2) | element-plus;移动端 mint-ui → vant 4 等 |
| 事件总线 | 自建/Vue | mitt 或 provide/inject |
| 富文本/图表 | 老绑定库 | 找 Vue3 版封装或手写 onMounted 初始化 |
Vuex → Pinia(最重要的一次状态迁移)
// store/useCounterStore.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }), // 也能用 Options 风格,Vuex 用户友好
getters: { double: s => s.count * 2 },
actions: { inc() { this.count++ } }, // 不用 commit/mutation,直接改
})
// 组件里
import { useCounterStore } from '@/store'
const store = useCounterStore()
store.inc() // 直接调 action,内部随便改 state
- mutations 没了:action 里直接改 state(省一层样板);
- 支持 setup store(更像组合式函数,篇 5),也支持 Options store(迁移更顺);
- Pinia 自带 devtools 插件与 HMR;老 Vuex 的 modules 拆成多个
defineStore即可。
vue-router 3 → 4
// Vue2:new VueRouter({ mode:'history' }) + $router.push
// Vue3:函数式创建
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(), // 不再是 mode:'history'
routes: [ { path: '/', component: () => import('@/views/Home.vue') } ],
})
app.use(router)
- 组合式访问:
useRouter()/useRoute()(不再this.$router);onBeforeRouteLeave/Update守卫组合式可用; - 动态增删路由
addRoute/removeRoute、路由级 lazy 都一样,只是入口从类变成createRouter。
6. 迁移 runbook(照做清单)
- 冻结范围:拉出「用了 filters / listeners / 函数式组件 / Vuex / router3」的文件清单——它们就是全部工作量;
- 换构建:新入口用 Vite +
@vitejs/plugin-vue;逐步把组件文件拷过来,靠编译期报错驱动替换(Vue3 对很多不兼容点会直接警告/报错,别闷头猜); - 语言层清扫:按 §2/§3 逐个机械替换(过滤→函数、$set→直赋、v-model 形状→defineModel、.sync→v-model:xx、.native 删除);
- 状态与路由:Vuex → Pinia(可先 Options store 平迁),router 3 → 4;
- UI 库:element-ui → element-plus(组件名/事件名多数平移,个别 prop 变化查对照表);
- 验收:开
@vue/compat模式跑一遍,把 console 的不兼容警告清完;再关掉 compat。
心态:迁移 90% 是机械替换,难点只在「全局注册怎么收进 app」和「状态/路由换库」两块。别一次性重写业务,Vue3 兼容 Vue2 的老 Options 写法,先平移后重构最稳。
关联
- 上一篇:Composition API 与 script setup
- 下一篇(想懂原理再动手):响应式原理
- 相关:本站 zustand/Pinia 类状态管理探索(同为「状态=组合式 store」的心智)
参考
- 官方迁移指南(逐条对照表):v3-migration.vuejs.org
- Vuex → Pinia 迁移:pinia.vuejs.org
- vue-router 4 文档:router.vuejs.org