Vue 2.7 和 Vue 3.5 到底怎么选?聊聊老项目升级前没人告诉你的代价
先说背景。我手上维护一个老后台管理系统,从 2018 年写到现在,一共有 80 多个业务页面、30 多个共享组件,代码量算上依赖大概 50 万行。我们现在还在用 Vue 2.7,不是不知道 Vue 3 已经到 3.5 了,也不是没体验过 Vite 到底有多快。
每次考虑升级,我都会想到前年给一个新项目上 Vue 3 + Element Plus,光在 vue-tsc 的类型报错上就卡了一个下午,晚上又因为 onMounted 里访问一个 ref 的时机和我旧习惯不一样,调 bug 调到快两点。
网上大部分吹 Vue 3 的文章,默认前提都是新项目,没有历史包袱,没有不知道为什么要写的 mixin,更不用担心业务方突然要求兼容内网旧浏览器。真实项目不是那样的。
所以我今天想聊的可能有点不好听:在 Vue 2.7 和 Vue 3.5 之间,不只是新和旧的对比,还有一堆没人直说的维护代价。
Composition API 与 Options API:真正的麻烦不是选项式,而是复用方式
很多文章夸 Vue 3,第一个理由必然是 Composition API。单看代码示例确实干净,比如一个地址选择器的逻辑,用 Vue2 常规写法一般是抽个 addressMixin,里面放 selectedAddress、shipAddressList、fetchAddress,再通过 $emit 通知外层。 问题是 mixin 的注入是扁平的,一旦两个 mixin 都定义了 handleSelect,后加载的会覆盖先加载的,找原因要翻好几个文件。用 Composition API 封装成 useAddressSelector,把数据和操作用函数返回,组件只负责渲染,这确实是进步。 但进步有代价。Composition API 不强制组织结构,setup() 里可以写 200 行混乱代码,也可以用一堆 watch 写到飞起。团队里只要有一个习惯用命令式写后端逻辑的人,很容易就把响应式数据声明到 watcher 下面,初始化顺序混乱,比原来 data 里明明白白的属性难维护得多。 更尴尬的是 Vue 2.7 的 Composition API 是从 Vue 3 backport 过来的,没有原生 Proxy,也没有完整的生命周期对齐。比如 watch 一个 props 里的嵌套对象时,它在某些边界情况下的 immediate 表现和 Vue 3 并不完全一致。你心里要记两套兼容差异,写代码的时候总得犹豫一下。
Proxy 响应式很先进,但调试体验不一定更好
Vue3 最大的底层变化,是用 Proxy 替换了 Object.defineProperty。这能拦截到新增属性和数组下标,理论上比 Vue2 精细。但实际业务场景里,一个页面 80% 的时间都耗在 DOM re-render 和接口返回的大数组深拷贝上,响应式那一层又占了多少?
我在维护项目时曾经找过一个 2000 行表格列表页,分别用 Vue2.7 和 Vue3.5 写了两个最小复刻版本,响应式初始化耗时相差不到 15ms,用户完全没体感。真正能感觉出来的反而是打包产物体积,按需引用的 Vue3 版本比原来少了大概 18%,在小带宽环境里第一次加载快一点。
Proxy 还带来一个麻烦:你在 console 里打印一个响应式数据,看到的是 Proxy 对象,想展开它又没法直接看到原始值,必须 toRaw。有次排查一个深层次对象变更问题,我在浏览器里点了半天,后来只能 JSON.parse(JSON.stringify(state)) 打出来才定位到是哪一层被改乱的。Vue2 的响应式虽然笨,但至少你看到的 data 是普通对象,能明显看到哪些属性被 defineProperty 加了 getter/setter。
如果真要升 Vue3,别只看 Demo,先算一笔账
老项目迁移不是把 package.json 里的 vue 版本改成 3 就能跑的。按我实际踩过的坑,至少要过以下七关:
- vue-router 要从 3 升到 4,写法从 new Router 变成 createRouter,动态路由和路由守卫的响应式判断全部要重新测试。
- Vuex 不升级的话类型不友好,升到 Vuex4 又觉得不如直接转 Pinia,而转 Pinia 意味着每个页面的 mapState 都要删掉,改成按需引入 store。
- 老项目的 element-ui 得换成 element-plus,组件 API 不是一一对应的,比如 Pagination 的
:current-page.sync改成了v-model:current-page,不是全局替换能解决的。 - 模板里所有 filter 都没了,要么写成方法调用,要么事先计算好字段,涉及模板的地方基本都要扫描一遍。
$on、$off也被移除,老的自定义事件总线要么换 mitt,要么封装一个兼容库,还得保证事件重复销毁时不泄漏。- 自定义指令的钩子名字从 bind/unbind 改成 mounted/unmounted,指令里的 this 也没了,要重新设计上下文。
- 自定义组件上的 v-model 默认行为从 value/input 变成 modelValue/update:modelValue,所有自己封装的双向绑定组件都得改。
假设你手里是 80 个页面的后台系统,我粗略做了一次影响面统计,大概有 30 个页面会碰到上面这些变化,加上公共代码和第三方依赖,至少 60 个文件要动。两个人熟悉业务的情况下,最快也要 12 个工作日去改,再算上回归测试和业务方扯皮,三周能上线就是很理想了。 所以我的想法很简单:如果项目还在快速迭代且没有刚需,不升级真不是技术落后,是成本收益比太差。等什么时候要重构模块了,拿新模块试试 Vue3,用渐变方式替代大版本跳跃,这才是适合大多数老项目走的路。