把 Vue 2 老项目搬上 Vue 3:compat 迁移层、Vite 白屏问题与 7 步顺序
Vue 2 在 2023-12-31 正式 EOL。真正拖时间的环节是围绕框架的一圈生态:长期未更新的插件、2019 年固定下来的构建工具、悄悄依赖 Vue 2 内部实现的测试设置、团队肌肉记忆里的隐性知识。框架语法本身通常不难处理——Composition API 是 opt-in,模板语法基本没变。
把目标定清楚:最小可交付目标 = Vue 2 语法跑在 Vue 3 引擎上。Composition API 重写是另一个独立项目,在后续日常迭代中逐个组件做。把两者压进同一个立项阶段,是最常见的迁移停滞原因——团队一边和构建错误缠斗,一边试图重新设计每个组件,一个季度下来无法上线。
迁移构建是气闸,不是终点
官方 @vue/compat 让 Vue 2 代码跑在 Vue 3 上,并且每碰到一处不兼容就在控制台输出一条 warning。它是 Vue 3 的 alias:
// vite.config.js
export default {
resolve: {
alias: { vue: '@vue/compat' },
},
optimizeDeps: { exclude: ['vue'] },
}
安装好后按组件或全局设置 compat 开关,然后看控制台不停输出弃用警告。
需要注意的是,迁移构建不是终点,只是过渡阶段。代码库在 compat 模式里停留时间越长,新代码越会悄悄依赖 Vue 2 语义,最终彻底升级时改动范围就越大。需要设置移除日期,将未解决 warning 挂到任务清单,然后按时删掉 alias。
无法一次完成迁移时,Vue 2.7 可用于过渡。它后向移植了 Composition API、`` 和大部分 Vue 3 类型系统。先在 Vue 2.7 上向 Composition API 靠,再换引擎。Vue 2.7 同样在 2023 年底 EOL,所以它只是中途一站,不能长期停留。
Vite 里 alias 到 @vue/compat 会白屏
一个 Vite 特有的问题:只按上面那行 alias: { vue: '@vue/compat' } 配完,页面很可能无法启动,控制台输出:
[Vue warn]: Component provided template option but runtime compilation is not supported in this build of Vue. Configure your bundler to alias "vue" to "vue/dist/vue.esm-bundler.js".
原因是 @vue/compat 包的默认入口指向 runtime-only 构建,不带模板编译器。Vue 2 时代不少组件依赖运行时编译字符串 template,换到 compat 后就没有编译器可用。
修法两件事:alias 指到带编译器的入口 @vue/compat/dist/vue.esm-bundler.js;再让 @vitejs/plugin-vue 的模板编译器开启 compat 模式,MODE: 2 是全开,也可逐项关:
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [
vue({
template: {
compilerOptions: {
compatConfig: { MODE: 2 },
},
},
}),
],
resolve: {
alias: {
vue: '@vue/compat/dist/vue.esm-bundler.js',
},
},
optimizeDeps: {
exclude: ['vue'],
},
})
会撞上的六类移除 API
按「现象 → 替代写法」排,基本是它们在老项目里咬人的顺序。
Filters。 {{ price | currency }} 不再编译。改成 computed 或方法调用;变换很机械,按模块一个 PR 一个 PR 走。
// 替代
{{ formatCurrency(price) }}
$listeners 并入 $attrs。 原来 v-on="$listeners" 的组件要换 v-bind="$attrs"(现在会一并包含 onClick、onSubmit 这类监听)。配合新的 inheritAttrs: false 语义,根元素上的转发是主要测试点。
Slot 语法。 旧的 slot / slot-scope 属性被移除,换成 v-slot:name="props" 和 #name="props" 简写。行为一致,只是写法变了。
…
…
函数式 SFC。 functional: true 的单文件组件不再存在。普通函数组件还能用,但多数团队直接改回常规 SFC。
Vue.set / Vue.delete。 Vue 3 的响应式基于 Proxy,这两个兼容 helper 不再需要,直接赋值、直接 delete 即可。
// Vue 2
Vue.set(obj, 'key', value)
Vue.delete(obj, 'key')
// Vue 3
obj.key = value
delete obj.key
new Vue() 事件总线失效。 Vue 不再是那个构造函数。换成 mitt、一个 Pinia store,或者更常见的做法——props 和 emits。
// Vue 2
const bus = new Vue()
// Vue 3
import mitt from 'mitt'
const bus = mitt()
迁移构建对上面每一条都会输出 console warning。把所有 warning 当作任务清单:分类、归组、批量处理完。
Vuex 到 Pinia 是重构,不是改名
Vuex 4 支持 Vue 3,所以不迁 Pinia 也能上线。但 Pinia 现在是官方推荐的 store,类型推断好一大截,setup 风格的 store 写起来就是普通 composable:
// stores/auth.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useAuthStore = defineStore('auth', () => {
const user = ref(null)
const isAuthenticated = computed(() => user.value !== null)
async function login(email: string, password: string) {
user.value = await api.login(email, password)
}
function logout() {
user.value = null
}
return { user, isAuthenticated, login, logout }
})
实际可行的迁法:Vuex 保持安装不动,并行引入 Pinia,从依赖图的叶子节点往上搬模块。不要先动全局购物车,先改没人碰的 settings 模块。团队先熟悉这种组织形态,再动高频部分。
工具链占迁移的一半
插件生态才是拖慢升级速度的主要原因。Vue 版本本身通常不是主要瓶颈。
Vetur 是老的 VS Code 扩展,已被 Volar(现称 Vue Language Tools)取代。必须卸载 Vetur:两个扩展对模板解析的规则不一致,会在难以察觉的地方浪费大量调试时间。
ESLint 插件要升到能理解 Composition API 和 `` 的版本。eslint-plugin-vue 针对 Vue 3 是一套不同的规则集,不能假设 .eslintrc 可以直接平移。
测试设置最容易消耗时间。@vue/test-utils v2 是 Vue 3 版本,v1 对应 Vue 2。挂载 API 看着像,其实不完全一样——propsData 改名 props,插槽和全局 plugin 的挂载方式变了。测试套件要尽早跑,为改写留预算,通常是小改,偶尔动结构。
Composition API 重构是可选项
Vue 3 不要求 Composition API。Options API 仍然能用、仍有完整类型支持,也仍是 Vue 3 文档里的一等风格。除了上面那些 breaking change,一个 Vue 3 代码库可以和 Vue 2 长得几乎一样。
所以「最小迁移 = Vue 2 语法 + Vue 3 引擎」是一个真实可交付的目标。Composition API 重写单独立项,碰到哪个组件迁哪个。测试要跟着,超出下一个功能边界的部分先停手。
能跑通的 7 步顺序
- 先升工具链。 Volar、ESLint、
@vue/test-utils、Vite(或 Vue CLI 支持 Vue 3 的最新版本)。在现有 Vue 2 代码上把新构建跑绿。 - 审计依赖。
package.json里每个 Vue 插件都要有兼容 Vue 3 的版本。列出没有的那些,决定替换、fork 还是删掉。这一步通常会冒出 2–3 个真正卡住升级、需要实际投入的插件。 - 切迁移构建。 盯 warning,按类别批量修——filters、slots、listeners。
- 跑测试。 修测试改写。测试套件绿之前不发版。
- 迁 router。 Vue Router 4 是 Vue 3 版本。API 表面相似,安装方式和类型定义不一样。
- 迁 store。 必须要的话先 Vuex 4;能上就 Pinia。
- 去掉迁移构建。 alias 从
@vue/compat切回vue,修掉所有依赖 compat 层的地方。
然后才开始 Composition API 重构。按组件、按功能,跟着正常迭代节奏走。大爆炸式重写在规划会上很果断,到第三周就散架;增量顺序交付的小改动可审查、可回滚。
迁移暴露的是本来就难维护的代码
每次迁移都会暴露同一件事:最难迁的代码本来就是最难维护的代码。紧耦合的 mixins、直接从 Vuex 内部 state 读数据的组件、随手改响应式对象的老 helper——原作者自己都未必记得当初为什么这么写。
Vue 3 在几处更严格:Proxy 响应式抓住了一些 Vue 2 的 Object.defineProperty 悄悄容忍的写法。这种严格是好事。上一次迁移里挂掉的组件,多半三年前就该出问题。Vue 3 只是把它们变成了编译错误。