vue-refactor
vue-refactor
Vue 代码里最常见的三种"该拆":主干太胖、UI 和 IO 纠缠、响应式和业务纠缠。这个 skill 是陪练——先看代码给诊断,再选对应处方,按编译器驱动的方式一步步搬。不写测试防护网,靠编译器 + 单步可回滚保证不改变行为。
方法论出处:Arlo Belshee 的 "Provable Refactorings"、Michael Feathers 的 "Lean on the Compiler"、Michael Thiessen 的 Humble Component + Thin Composable。详细哲学见 reference/compiler-driven-principles.md。
核心纪律(四条,每次都要遵守)
- 行为等价是底线。这次动作不改变任何外部可观察行为——DOM 输出、事件时序、网络请求、store 变化、路由跳转全都要等价。一旦发现顺手会"优化"某个行为,停下,拆出去走 feature 或 issue。
- 每步后编译器必须绿。包括
tsc --noEmit、Volar 类型检查、ESLint、已有的单测。任何一步编译不过就立刻回退这一步,不要"先留着,后面一起修"。 - 一步一个语义单元。一次只搬一个字段、一个方法、一个模板块。不合并、不打包、不"顺手带一个"。
- 创建新 → 替换引用 → 删除旧。永远这个三步循环。不要原地改名(改名会让引用一次性全断),要先建新位置、再一个个搬引用、最后拆旧位置。
违反任意一条都等于回到"AI 胡乱重构"——这个 skill 的存在意义就没了。
先跑前置检查(命中就停,给路由)
进诊断之前确认四件事,任一命中就中止 skill,给路由建议,不硬上:
| 检查 | 命中时路由到 |
|---|---|
| 用户想同时改行为(加功能 / 修 bug / 调样式出效果) | 走 feature(cs-feat)或 issue(cs-issue) |
| 代码量 > 800 行单文件 或 > 3 个文件同时要动 | 劝用户先缩范围到一个 SFC / 一个 composable |
| 项目没配 TypeScript + Volar,纯 JS Vue 项目 | 告警:编译器驱动的保证会弱一大截,改动要靠 grep + 手动跑全量测试;仍可继续但风险提一档 |
| 用户没决定改动范围就说"你看着办" | 反问"这次是只拆 X 组件,还是顺带它依赖的也一起?"——范围定了再进诊断 |
如果用户根本没给具体代码路径,先问:"这次拆哪个文件?贴路径或代码片段。"
诊断决策树
拿到代码后跑一遍四个判断。匹配到哪一条就用哪张处方;能匹配多条时按顺序优先(1 > 2 > 3)。
判断 1:主干是否太胖?→ 处方 A(水落石出外移)
信号(任一命中):
<script setup>超过 200 行- 圈复杂度目测 > 10(多层嵌套 if / switch / try)
- 一个组件里同时有 > 5 个 ref/reactive + > 5 个 method + > 3 个 watch/computed
- 能一眼在里面指出"这几个变量是一个局部主题"(比如都围绕"表单校验"或"拖拽状态")
→ 用处方 A:把这一簇变量及其方法外移到新 composable 或 util 模块。详细见 reference/recipe-outward-move.md。
判断 2:UI 和 IO/业务是否纠缠?→ 处方 B(Humble/Controller 拆分)
信号(任一命中):
- 同一个组件里既有
<template>大段渲染,又有fetch / axios / useQuery / store.dispatch调用 - 同一个组件里既直接操作 props 转展示,又处理
router.push / localStorage / window.xxx - 测试时必须 mount 整个组件 + stub network 才跑得动——没办法只测渲染
- 子组件间有"纯展示型候选"——模板里明显可以切出一块只靠 props 和 emit 通信的区域
→ 用处方 B:把纯展示层切成 Humble 子组件(props 进 / emit 出,零 ref/IO),父组件留作 Controller 调度。详细见 reference/recipe-humble-controller.md。
判断 3:响应式和业务逻辑是否纠缠?→ 处方 C(Thin Composable 提纯)
信号(任一命中):
- composable 里业务计算(算折扣、校验、格式化、状态转换)和
ref / watch / computed混写 - 想单测业务计算时,必须
mount或mock reactivity才能跑 - 同样的计算逻辑在多个 composable / 组件里各抄一份
- 能一眼指出"这几行代码不依赖任何 ref,只是纯计算"
→ 用处方 C:把业务逻辑抽成纯函数放到 lib/,composable 只留一层响应式薄壳(解包 → 调纯函数 → 塞回 ref)。详细见 reference/recipe-thin-composable.md。
判断 4:三者都命中 → 选最外层先做
如果三个信号全中,说明代码在三个维度都腐化了。按 A → B → C 顺序做:
- 先 A 把主干拆薄(暴露出哪些部分是 UI、哪些是 IO、哪些是业务)
- 再 B 把 UI 独立(让 Controller 的职责清晰)
- 最后 C 把业务从响应式里提纯
每一次只做一张处方的一轮,做完停下让用户看效果再决定要不要进下一张。
三张处方的共同骨架(每张都遵守这个节拍)
无论 A/B/C,执行节拍都是同一个:
1. 定位一个「语义单元」(一簇变量 / 一块模板 / 一段纯逻辑)
2. 新建目标位置(空文件 / 空 composable / 空子组件的 stub),保持编译绿
3. 把第一个成员搬过去(通常是一个字段 / 一个 prop / 一个函数签名)
↓
编译必然报错:原位置引用它的地方全断了
↓
**把这些错当成待办清单**——编译器告诉你的就是所有调用点
4. 逐个消错:要么把引用指向新位置,要么把依赖它的方法也一起搬过去
5. 这一步所有错消完 → 提交 / 标记可回滚点
6. 回到 1,搬下一个成员
7. 当原位置清空或只剩 re-export / delegate 时,做最后一次删除
关键直觉:每次"把一个成员从 A 搬到 B"时,你不是在"改对的代码",你是在让编译器告诉你"哪些地方还在用这个成员"。编译错是免费的静态分析,不要抗拒它——拥抱它、用它当检查表。
如果某一步消错消到一半遇到了"这个方法依赖 3 个其他字段,而其中 2 个字段还没搬",停下,回退这一步,先去搬那 2 个字段,再回来。永远不要跨步骤累积半成品。
和用户协作的节奏
这是个陪练型 skill,不是自动化工具:
- 用户给代码 → 你做诊断,把判断结果讲给用户听("我觉得这是判断 1 命中,主干太胖,建议走处方 A;理由:…"),让用户确认或修正诊断
- 进处方后 → 先把"语义单元划分"讲给用户("我准备把这 3 个 ref + 这 2 个 method 当一簇搬出去,命名为
useXxx"),等用户点头 - 开始搬 → 每个语义单元搬完暂停汇报一次:"第一簇搬完了,编译绿,已提交 commit
abc123。下一簇是 …,继续吗?" - 遇到编译错消不掉 / 发现搬出去会改行为 → 立刻停下汇报,不要自己发挥
不要一口气做完三张处方再汇报。不要跳过某一簇的确认。陪练的意义在于每一步都留给人一个决策机会。
容易踩的坑
- 靠重命名代替搬运:
Rename Symbol会把所有引用一次性换掉,看起来没报错,但也失去了"编译器标出调用点"这个检查表。永远是新建 → 复制搬 → 逐个改引用 → 删旧。 - 一次搬一整个 composable:看着是"一个语义单元"其实里面有 10 个 ref。拆不动编译器的错。要按字段一个个搬。
- 在重构中间修类型错误/补类型注解:诱惑很大("反正编译过了顺手补一下"),但这改变了类型契约,等于改行为。类型收紧要拆成独立改动。
- 把
watch/watchEffect搬到纯函数里:纯函数里不能出现响应式 API。要么留在 composable 壳里,要么把 watch 的回调体抽成纯函数再在 watch 里调。 - Humble 组件里偷偷摸 store / route:只要伸手进了 pinia / vue-router,它就不 humble 了。发现就立刻退回来,这块状态应该在 Controller 里取好再 props 下来。
- 跨处方混做:做着 A 顺手拆了个 Humble;做着 C 顺手把主干也瘦了身。看起来高效,实际让每一步的"退出信号"变模糊、回滚成本变高。一轮一张处方。
- Vue 2 Options API 直接跳到
<script setup>:那是迁移不是重构,行为等价无法靠编译器证明(Options 的this绑定和 setup 的闭包语义不同)。要拆成"先重构掉胖主干(还在 Options 里)→ 再迁移到 setup"两个独立任务。
退出条件
一次会话结束前至少满足:
- 诊断结论用户明确认可
- 至少一个语义单元完整搬完(新位置可用、旧位置清空或只剩 delegate、编译 + 已有测试绿)
- 每个完整搬迁点都有可回滚的 commit(commit message 带明确的"move X from Y to Z"语义)
- 如果中途停下,明确告诉用户当前停在哪个语义单元的第几步,下次可以续上
如果用户喊停或一轮处方走完,做一次小结:搬了哪些单元、新建了哪些文件、下一步可选的是什么处方。
相关文档
reference/compiler-driven-principles.md— 为什么靠编译器而不是靠测试 / Arlo 的 Provable Refactorings 哲学浓缩reference/recipe-outward-move.md— 处方 A:水落石出外移(胖主干 → 多个 composable/util)reference/recipe-humble-controller.md— 处方 B:Humble/Controller 拆分(UI 和 IO 解耦)reference/recipe-thin-composable.md— 处方 C:Thin Composable 提纯(纯函数 + 响应式薄壳)