vue-refactor
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinesevue-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.mdThe three most common "need-to-split" scenarios in Vue code: Fat trunk, UI entangled with IO/business logic, reactivity entangled with business logic. This skill acts as a coach — first diagnose the code, then select the corresponding recipe, and move step by step in a compiler-driven way. No test safeguards are written; behavioral equivalence is guaranteed by the compiler + step-by-step rollback.
Methodology sources: Arlo Belshee's "Provable Refactorings", Michael Feathers' "Lean on the Compiler", Michael Thiessen's Humble Component + Thin Composable. Detailed philosophy can be found in .
reference/compiler-driven-principles.md核心纪律(四条,每次都要遵守)
Core Disciplines (Four Rules to Follow Every Time)
- 行为等价是底线。这次动作不改变任何外部可观察行为——DOM 输出、事件时序、网络请求、store 变化、路由跳转全都要等价。一旦发现顺手会"优化"某个行为,停下,拆出去走 feature 或 issue。
- 每步后编译器必须绿。包括 、Volar 类型检查、ESLint、已有的单测。任何一步编译不过就立刻回退这一步,不要"先留着,后面一起修"。
tsc --noEmit - 一步一个语义单元。一次只搬一个字段、一个方法、一个模板块。不合并、不打包、不"顺手带一个"。
- 创建新 → 替换引用 → 删除旧。永远这个三步循环。不要原地改名(改名会让引用一次性全断),要先建新位置、再一个个搬引用、最后拆旧位置。
违反任意一条都等于回到"AI 胡乱重构"——这个 skill 的存在意义就没了。
- Behavioral equivalence is the bottom line. This action must not change any externally observable behavior — DOM output, event timing, network requests, store changes, route jumps must all be equivalent. If you find yourself wanting to "optimize" a behavior in passing, stop and handle it via the feature or issue process.
- The compiler must be green after each step. This includes , Volar type checking, ESLint, and existing unit tests. If any step fails to compile, roll back this step immediately — do not "leave it for later to fix together".
tsc --noEmit - One semantic unit per step. Move only one field, one method, or one template block at a time. Do not merge, package, or "take one along in passing".
- Create new → Replace references → Delete old. Always follow this three-step cycle. Do not rename in place (renaming will break all references at once); instead, first create the new location, then move references one by one, and finally delete the old location.
Violating any of these rules means reverting to "AI random refactoring" — the purpose of this skill is lost.
先跑前置检查(命中就停,给路由)
Pre-Checks First (Stop and Route if Hit)
进诊断之前确认四件事,任一命中就中止 skill,给路由建议,不硬上:
| 检查 | 命中时路由到 |
|---|---|
| 用户想同时改行为(加功能 / 修 bug / 调样式出效果) | 走 feature(cs-feat)或 issue(cs-issue) |
| 代码量 > 800 行单文件 或 > 3 个文件同时要动 | 劝用户先缩范围到一个 SFC / 一个 composable |
| 项目没配 TypeScript + Volar,纯 JS Vue 项目 | 告警:编译器驱动的保证会弱一大截,改动要靠 |
| 用户没决定改动范围就说"你看着办" | 反问"这次是只拆 X 组件,还是顺带它依赖的也一起?"——范围定了再进诊断 |
如果用户根本没给具体代码路径,先问:"这次拆哪个文件?贴路径或代码片段。"
Confirm four things before entering diagnosis; stop the skill and provide routing suggestions if any are hit, do not force it:
| Check | Route to when hit |
|---|---|
| User wants to change behavior at the same time (add features / fix bugs / adjust styles for effects) | Follow feature (cs-feat) or issue (cs-issue) process |
| Code size > 800 lines per single file or > 3 files need to be modified simultaneously | Advise the user to narrow the scope to one SFC / one composable first |
| Project is not configured with TypeScript + Volar, pure JS Vue project | Warning: Compiler-driven guarantees will be significantly weakened; changes rely on |
| User says "You decide" without determining the scope of changes | Ask back: "Are we only splitting component X this time, or including its dependencies as well?" — enter diagnosis only after the scope is confirmed |
If the user does not provide a specific code path at all, first ask: "Which file are we splitting this time? Paste the path or code snippet."
诊断决策树
Diagnosis Decision Tree
拿到代码后跑一遍四个判断。匹配到哪一条就用哪张处方;能匹配多条时按顺序优先(1 > 2 > 3)。
Run four checks after receiving the code. Use the corresponding recipe if matched; if multiple matches exist, prioritize in order (1 > 2 > 3).
判断 1:主干是否太胖?→ 处方 A(水落石出外移)
Check 1: Is the trunk too fat? → Recipe A (Outward Move to Reveal Core)
信号(任一命中):
- 超过 200 行
<script setup> - 圈复杂度目测 > 10(多层嵌套 if / switch / try)
- 一个组件里同时有 > 5 个 ref/reactive + > 5 个 method + > 3 个 watch/computed
- 能一眼在里面指出"这几个变量是一个局部主题"(比如都围绕"表单校验"或"拖拽状态")
→ 用处方 A:把这一簇变量及其方法外移到新 composable 或 util 模块。详细见 。
reference/recipe-outward-move.mdSignals (any hit):
- exceeds 200 lines
<script setup> - Cyclomatic complexity is visually > 10 (multi-layer nested if / switch / try)
- A component has > 5 ref/reactive + > 5 methods + > 3 watch/computed at the same time
- You can point out "these variables are a local theme" at a glance (e.g., all around "form validation" or "drag state")
→ Use Recipe A: Move this cluster of variables and their methods to a new composable or util module. Details can be found in .
reference/recipe-outward-move.md判断 2:UI 和 IO/业务是否纠缠?→ 处方 B(Humble/Controller 拆分)
Check 2: Is UI entangled with IO/business logic? → Recipe B (Humble/Controller Split)
信号(任一命中):
- 同一个组件里既有 大段渲染,又有
<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.mdSignals (any hit):
- A component has both large rendering and
<template>callsfetch / axios / useQuery / store.dispatch - A component both directly converts props for display and handles
router.push / localStorage / window.xxx - Testing requires mounting the entire component + stubbing the network to run — cannot test rendering alone
- There are "pure display candidates" among child components — a section can be clearly cut out from the template that only communicates via props and emit
→ Use Recipe B: Split the pure display layer into Humble child components (props in / emit out, zero ref/IO), and keep the parent component as a Controller for scheduling. Details can be found in .
reference/recipe-humble-controller.md判断 3:响应式和业务逻辑是否纠缠?→ 处方 C(Thin Composable 提纯)
Check 3: Is reactivity entangled with business logic? → Recipe C (Thin Composable Purification)
信号(任一命中):
- composable 里业务计算(算折扣、校验、格式化、状态转换)和 混写
ref / watch / computed - 想单测业务计算时,必须 或
mount才能跑mock reactivity - 同样的计算逻辑在多个 composable / 组件里各抄一份
- 能一眼指出"这几行代码不依赖任何 ref,只是纯计算"
→ 用处方 C:把业务逻辑抽成纯函数放到 ,composable 只留一层响应式薄壳(解包 → 调纯函数 → 塞回 ref)。详细见 。
lib/reference/recipe-thin-composable.mdSignals (any hit):
- Business calculations (discount calculation, validation, formatting, state conversion) are mixed with in a composable
ref / watch / computed - To unit test business calculations, you must or
mountto runmock reactivity - The same calculation logic is copied in multiple composables / components
- You can point out "these lines of code do not rely on any ref, they are just pure calculations" at a glance
→ Use Recipe C: Extract business logic into pure functions placed in , and leave only a thin reactivity shell in the composable (unwrap → call pure function → put back into ref). Details can be found in .
lib/reference/recipe-thin-composable.md判断 4:三者都命中 → 选最外层先做
Check 4: All three are hit → Start with the outermost one
如果三个信号全中,说明代码在三个维度都腐化了。按 A → B → C 顺序做:
- 先 A 把主干拆薄(暴露出哪些部分是 UI、哪些是 IO、哪些是业务)
- 再 B 把 UI 独立(让 Controller 的职责清晰)
- 最后 C 把业务从响应式里提纯
每一次只做一张处方的一轮,做完停下让用户看效果再决定要不要进下一张。
If all three signals are hit, the code is corrupted in three dimensions. Follow the order A → B → C:
- First use A to thin the trunk (reveal which parts are UI, which are IO, which are business logic)
- Then use B to separate the UI (clarify the Controller's responsibilities)
- Finally use C to purify business logic from reactivity
Only complete one round of one recipe at a time; stop after completion to let the user see the effect before deciding whether to proceed to the next recipe.
三张处方的共同骨架(每张都遵守这个节拍)
Common Skeleton for All Three Recipes (Follow This Rhythm Every Time)
无论 A/B/C,执行节拍都是同一个:
1. 定位一个「语义单元」(一簇变量 / 一块模板 / 一段纯逻辑)
2. 新建目标位置(空文件 / 空 composable / 空子组件的 stub),保持编译绿
3. 把第一个成员搬过去(通常是一个字段 / 一个 prop / 一个函数签名)
↓
编译必然报错:原位置引用它的地方全断了
↓
**把这些错当成待办清单**——编译器告诉你的就是所有调用点
4. 逐个消错:要么把引用指向新位置,要么把依赖它的方法也一起搬过去
5. 这一步所有错消完 → 提交 / 标记可回滚点
6. 回到 1,搬下一个成员
7. 当原位置清空或只剩 re-export / delegate 时,做最后一次删除关键直觉:每次"把一个成员从 A 搬到 B"时,你不是在"改对的代码",你是在让编译器告诉你"哪些地方还在用这个成员"。编译错是免费的静态分析,不要抗拒它——拥抱它、用它当检查表。
如果某一步消错消到一半遇到了"这个方法依赖 3 个其他字段,而其中 2 个字段还没搬",停下,回退这一步,先去搬那 2 个字段,再回来。永远不要跨步骤累积半成品。
Regardless of A/B/C, the execution rhythm is the same:
1. Locate a "semantic unit" (a cluster of variables / a template block / a section of pure logic)
2. Create the target location (empty file / empty composable / stub of empty child component), keep the compiler green
3. Move the first member over (usually a field / a prop / a function signature)
↓
The compiler will definitely throw errors: all references to it in the original location are broken
↓
**Treat these errors as a to-do list** — the compiler tells you all the call points
4. Fix errors one by one: either point the reference to the new location, or move the dependent methods over together
5. After all errors in this step are fixed → Commit / mark a rollback point
6. Return to step 1, move the next member
7. When the original location is empty or only has re-export / delegate, perform the final deletionKey Insight: Every time you "move a member from A to B", you are not "correcting code"; you are letting the compiler tell you "where else this member is being used". Compilation errors are free static analysis — do not resist them, embrace them and use them as a checklist.
If during error fixing you encounter "this method depends on 3 other fields, and 2 of them haven't been moved yet", stop, roll back this step, move those 2 fields first, then come back. Never accumulate half-finished work across steps.
和用户协作的节奏
Collaboration Rhythm with Users
这是个陪练型 skill,不是自动化工具:
- 用户给代码 → 你做诊断,把判断结果讲给用户听("我觉得这是判断 1 命中,主干太胖,建议走处方 A;理由:…"),让用户确认或修正诊断
- 进处方后 → 先把"语义单元划分"讲给用户("我准备把这 3 个 ref + 这 2 个 method 当一簇搬出去,命名为 "),等用户点头
useXxx - 开始搬 → 每个语义单元搬完暂停汇报一次:"第一簇搬完了,编译绿,已提交 commit 。下一簇是 …,继续吗?"
abc123 - 遇到编译错消不掉 / 发现搬出去会改行为 → 立刻停下汇报,不要自己发挥
不要一口气做完三张处方再汇报。不要跳过某一簇的确认。陪练的意义在于每一步都留给人一个决策机会。
This is a coaching-type skill, not an automation tool:
- User provides code → You diagnose, explain the judgment result to the user ("I think Check 1 is hit, the trunk is too fat, recommend Recipe A; reason: …"), let the user confirm or correct the diagnosis
- After entering the recipe → First explain the "semantic unit division" to the user ("I plan to move this cluster of 3 refs + 2 methods out, named "), wait for the user's approval
useXxx - Start moving → Pause and report after each semantic unit is moved: "The first cluster has been moved, compiler is green, commit has been submitted. The next cluster is …, continue?"
abc123 - If you cannot fix a compilation error / find that moving will change behavior → Stop and report immediately, do not improvise
Do not complete all three recipes at once before reporting. Do not skip confirmation for any cluster. The meaning of coaching is to leave a decision opportunity for the user at every step.
容易踩的坑
Common Pitfalls
- 靠重命名代替搬运:会把所有引用一次性换掉,看起来没报错,但也失去了"编译器标出调用点"这个检查表。永远是新建 → 复制搬 → 逐个改引用 → 删旧。
Rename Symbol - 一次搬一整个 composable:看着是"一个语义单元"其实里面有 10 个 ref。拆不动编译器的错。要按字段一个个搬。
- 在重构中间修类型错误/补类型注解:诱惑很大("反正编译过了顺手补一下"),但这改变了类型契约,等于改行为。类型收紧要拆成独立改动。
- 把 /
watch搬到纯函数里:纯函数里不能出现响应式 API。要么留在 composable 壳里,要么把 watch 的回调体抽成纯函数再在 watch 里调。watchEffect - Humble 组件里偷偷摸 store / route:只要伸手进了 pinia / vue-router,它就不 humble 了。发现就立刻退回来,这块状态应该在 Controller 里取好再 props 下来。
- 跨处方混做:做着 A 顺手拆了个 Humble;做着 C 顺手把主干也瘦了身。看起来高效,实际让每一步的"退出信号"变模糊、回滚成本变高。一轮一张处方。
- Vue 2 Options API 直接跳到 :那是迁移不是重构,行为等价无法靠编译器证明(Options 的
<script setup>绑定和 setup 的闭包语义不同)。要拆成"先重构掉胖主干(还在 Options 里)→ 再迁移到 setup"两个独立任务。this
- Using renaming instead of moving: will replace all references at once, which seems error-free but loses the "compiler marks call points" checklist. Always create new → copy and move → modify references one by one → delete old.
Rename Symbol - Moving an entire composable at once: It looks like "one semantic unit" but actually contains 10 refs. You cannot handle the compiler errors. Move field by field.
- Fixing type errors / adding type annotations during refactoring: Tempting ("fix it while compiling anyway"), but this changes the type contract, which equals changing behavior. Type tightening should be split into independent changes.
- Moving /
watchinto pure functions: Reactive APIs cannot appear in pure functions. Either keep them in the composable shell, or extract the watch callback into a pure function and call it in the watch.watchEffect - Secretly accessing store / route in Humble components: As long as it accesses pinia / vue-router, it is no longer humble. Roll back immediately if found; this state should be fetched in the Controller and passed down via props.
- Mixing recipes during execution: Doing A and splitting a Humble component by the way; doing C and thinning the trunk by the way. It seems efficient, but actually blurs the "exit signal" of each step and increases rollback cost. One recipe per round.
- Directly jumping from Vue 2 Options API to : That is migration, not refactoring; behavioral equivalence cannot be proven by the compiler (Options'
<script setup>binding differs from setup's closure semantics). Split into two independent tasks: "First refactor the fat trunk (still in Options) → Then migrate to setup".this
退出条件
—
一次会话结束前至少满足:
- 诊断结论用户明确认可
- 至少一个语义单元完整搬完(新位置可用、旧位置清空或只剩 delegate、编译 + 已有测试绿)
- 每个完整搬迁点都有可回滚的 commit(commit message 带明确的"move X from Y to Z"语义)
- 如果中途停下,明确告诉用户当前停在哪个语义单元的第几步,下次可以续上
如果用户喊停或一轮处方走完,做一次小结:搬了哪些单元、新建了哪些文件、下一步可选的是什么处方。
—
相关文档
—
- — 为什么靠编译器而不是靠测试 / Arlo 的 Provable Refactorings 哲学浓缩
reference/compiler-driven-principles.md - — 处方 A:水落石出外移(胖主干 → 多个 composable/util)
reference/recipe-outward-move.md - — 处方 B:Humble/Controller 拆分(UI 和 IO 解耦)
reference/recipe-humble-controller.md - — 处方 C:Thin Composable 提纯(纯函数 + 响应式薄壳)
reference/recipe-thin-composable.md
—