2.6、2.7 与维护工具链:先确认运行的是谁
Vue 2.7 带来了内置 Composition API 和 script setup,但不是 Vue 3 的另一种版本号。它仍以 Vue 2 的实例、getter/setter 响应式和渲染体系为基础。旧项目里出现 setup 不能证明已经迁移到 Vue 3;仍用 Options 也不能证明它是 Vue 2。排错时先检查安装的运行时、模板编译器和构建插件,再检查组件风格。
Vue 2 已于 2023 年 12 月 31 日结束官方维护,2.7.16 是 Vue 2 的最终发布版本。本章固定这些版本以复现存量行为,不将它们当作新项目推荐环境。维护一个现有项目仍需要知道它原来如何工作,才能判断是修复、隔离、替换依赖还是迁移。
在相同输入下观察回移边界
在本章实验目录额外安装一个别名包 npm install --save-exact vue26@npm:vue@2.6.14。它不会替换原来的 vue@2.7.16,可以在同一 Node 文件中分别读取两个版本。保存 versions.cjs:
const Vue26 = require('vue26')
const Vue27 = require('vue')
console.log(Vue26.version, typeof Vue26.ref) // 2.6.14 undefined
console.log(Vue27.version, typeof Vue27.ref) // 2.7.16 function
const raw = { count: 1 }
const observed = Vue27.reactive(raw)
let evaluations = 0
const label = Vue27.computed(() => {
evaluations++
return observed.extra || 'empty'
})
console.log(observed === raw, label.value, evaluations) // true empty 1
observed.extra = 'added'
console.log(label.value, evaluations) // empty 1
// 替换为“从一开始就通过 set 新增”的独立对象,避免修复已存在裸字段。
const holder = Vue27.reactive({ item: {} })
const nextLabel = Vue27.computed(() => holder.item.extra || 'empty')
console.log(nextLabel.value) // empty
Vue27.set(holder.item, 'extra', 'added')
console.log(nextLabel.value) // addedreactive(raw) === raw 在 2.7 是 true,说明它改造原对象,没有建立 Vue 3 那样的代理身份。直接新增 extra 没有发出通知,因此缓存继续返回 empty。换用已观察容器中的 Vue.set 才走到新增属性的通知路径。API 名字相同,不意味着实现机制、对象身份或追踪范围完全相同。
本例的 computed 返回带 value 的引用,区别于 Options 里通过 vm.total 访问计算属性;其缓存目的相同。2.7 的 ref 也通过 value 暴露状态。若将 raw 直接当 reactive 的根数组,还存在缺少属性访问而无法追踪的限制;将数组放在稳定对象的字段上,或使用 ref 容器,并按 Vue 2 数组规则修改。Symbol 键和集合追踪也不能按 Vue 3 能力推断。
这里用的是 CommonJS 构建,因此新 API 位于 Vue27 对象上。在 2.7 的 ESM 构建中,应使用 import Vue, { ref } from 'vue',新 API 是具名导出,不能依赖 Vue.ref。模块格式是另一条维度,不要把导入方式错误误判为版本不支持。
哪些内容回移了,哪些没有
2.6 需要额外的 Composition API 插件才有对应能力;2.7 内置后应按迁移契约调整导入,避免同时安装两套实现。2.7 的 script setup 由 SFC 编译器处理,直接在普通浏览器 script 中写宏不会自动生效。它没有回移 createApp,也不支持 script setup 顶层 await 所需的异步初始化能力;Vue 2 的全局插件仍没有 Vue 3 的独立应用配置边界。
2.7 的 emits 主要提供类型检查信息,不会带来 Vue 3 那套运行时事件过滤行为。readonly、模板表达式支持等也应按确切 API 与编译入口核对,不能把“能写 Vue 3 风格”理解为“所有 Vue 3 特性都存在”。本书现代的 Vue 3.5 能力仍沿现代环境学习,不复制到这个旧运行时。
运行时、编译器和构建器各负责什么
运行时负责实例和更新。模板编译器把 template 变成 render 函数。vue-loader 或 Vite 插件则把 .vue 文件中的模板、脚本和样式送进各自的编译过程。应用报“只能运行渲染函数、不能编译模板”,未必是模板语法错,也可能是选中了 runtime-only 构建,却在运行时交给它 template 字符串。
Vue 2.6 常见的 SFC 链是 vue@2.6.x + vue-template-compiler@完全相同版本 + vue-loader@15。模板编译器与运行时版本不匹配可能在构建阶段直接报错。2.7 已把 SFC 编译相关能力纳入新配套,vue-loader 需要 15.10 或以上的 15.x;正常 SFC 构建不再依赖单独 vue-template-compiler,但部分旧测试工具仍会调用该包中的接口,不能不看测试依赖就删除。
维护时先在原项目中执行以下只读命令:
node --version
npm --version
npm ls vue vue-template-compiler vue-loader @vue/cli-service
npm ls @vitejs/plugin-vue2 @vue/composition-api vue-deminpm ls 可能因包缺失或 peer 冲突返回非零,需要读具体树和错误,不能把非零全部当作“项目无法运行”。再打开 lockfile 确认真正解析版本。只看 package.json 的 ^2.6 范围不够,因为它可能实际解析到 2.7。
保存原锁文件、原 Node/包管理器版本和当前可运行结果之后,才能在隔离分支做升级。不要先删除所有锁文件再试着碰运气重装;可以先明确更新某个依赖和相关锁项,把“补齐配套版本”和“重写组件”拆成可回退的小步。
一个可运行的配套检查
下面保存 compiler-pair.cjs。它接收已读取的版本对象,检查 Vue 2.6 的一个明确约束;它不冒充整个依赖兼容性求解器。
function checkPair({ vue, compiler }) {
if (!/^2\.6\.\d+$/.test(vue)) {
throw new Error('本检查只处理固定的 Vue 2.6 发布版本')
}
if (compiler !== vue) {
throw new Error(`模板编译器 ${compiler || '缺失'} 与运行时 ${vue} 不一致`)
}
return '配对通过'
}
for (const pair of [
{ vue: '2.6.14', compiler: '2.6.14' },
{ vue: '2.6.14', compiler: '2.6.12' },
{ vue: '2.7.16', compiler: '2.7.16' }
]) {
try { console.log(checkPair(pair)) }
catch (error) { console.log(error.message) }
}第一组通过,第二组报告不一致,第三组报告超出本检查范围。第三组不代表 2.7 不能工作,只表示不能把 2.6 的依赖规则机械应用于2.7。这种有范围的判断比输出一个笼统“兼容”更适合维护决策。
若切换到 Vite,官方 @vitejs/plugin-vue2 要求 Vue 2.7,不是 Vue 2.6 的通用替代品;JSX/TSX 又由专门的转换插件处理。旧 CLI 的 Node、webpack、Babel、PostCSS 插件也有各自的配套范围。更换构建器时需要验证资源地址、环境变量、路由刷新与目标浏览器,具体产物实验继续看工具链专题。本节不提供一套声称适配所有旧项目的“最新版本”安装命令。
练习:区分配套错误与范围外输入
给 checkPair 增加“缺少 vue 版本”的明确错误;输入 {}、{ vue:'2.6.14' }、正确配对、2.7 配对。分别观察缺少运行时、缺少编译器、不报错、范围外。
再在 versions.cjs 中把 holder.item 的整个对象替换成 { extra:'replaced' },观察 nextLabel.value 变为 replaced。解释为什么替换已有字段会通知,而直接新增字段不会;不要用“2.7 的 Proxy 自动处理”解释。