前言
重构这个话题,在这个系列里其实一直是"配角"——进阶指南的 9.3 节用了 5 行写了个流程清单,测试指南里把重构当作 TDD 循环的最后一步,Git 指南里提到了重构分支策略。但从来没有一篇文章正面回答过:怎么用 Claude Code 系统性地做重构。
提取函数怎么提?大文件怎么拆?类型系统怎么迁移?API 怎么做 breaking change?跨几十个文件的重命名怎么保证不漏?这些问题,今天一次讲清楚。
这篇文章和调试指南是姊妹篇——调试是"代码出了问题怎么修",重构是"代码没问题但可以更好"。两篇搭配着看效果更好。
一、为什么重构需要 AI 搭档
重构的风险与收益
重构的本质是"在不改变外部行为的前提下改善代码内部结构"。听起来很美好,但现实是:
| 收益 | 风险 |
|---|---|
| 提高可读性 | 引入回归 bug |
| 降低维护成本 | 打断当前开发节奏 |
| 改善性能 | 合并冲突增多 |
| 方便后续扩展 | 测试覆盖不足时无法验证 |
| 减少技术债 | 重构范围失控(越改越多) |
| 统一代码风格 | 团队成员不理解改动意图 |
这就是为什么很多团队"知道该重构,但一直不敢动"。
手动重构 vs Claude Code 重构
| 维度 | 手动重构 | Claude Code 重构 |
|---|---|---|
| 速度 | 一个函数 15-60 分钟 | 一个函数 2-5 分钟 |
| 影响范围分析 | 靠 IDE 搜索 + 经验 | 自动分析所有引用和依赖 |
| 风险控制 | 改完跑测试祈祷 | 每步重构后自动验证 |
| 一致性 | 改了这里忘了那里 | 全局统一修改 |
| 文档同步 | 经常忘记更新 | 可以同步更新注释和文档 |
| Code Review | 大 diff 难以审查 | 可以按步骤拆分 commit |
重构 vs 重写:怎么选?
| 信号 | 选择 |
|---|---|
| 核心逻辑正确,只是结构混乱 | 重构 |
| 测试覆盖率 > 60% | 重构(有安全网) |
| 技术栈需要整体迁移 | 重写 |
| 没有人理解现有代码 | 先让 Claude Code 分析,再决定 |
| 改动范围 < 30% 的代码 | 重构 |
| 改动范围 > 70% 的代码 | 重写 |
拿不准的时候,让 Claude Code 帮你评估:
> 分析 src/legacy/ 目录的代码质量:
> 1. 有多少文件超过 300 行?函数平均复杂度如何?
> 2. 有多少 any 类型?测试覆盖率大概多少?
> ��我一个"重构 vs 重写"的建议。二、重构前的准备
动手之前,先把安全网搭好。重构最怕的不是改错,而是改错了不知道。
检查测试覆盖率
没有测试的重构就是在走钢丝。详细的测试工作流参考测试指南:
# 先看覆盖率
> 跑一下测试覆盖率,重点看我要重构的 components/Navigation.tsx
# 覆盖率不够就先补
> Navigation.tsx 覆盖率只有 40%,帮我补充测试:
> - 桌面端导航链接渲染
> - 移动端 hamburger 菜单开关
> - 活跃链接高亮
> - Escape 键关闭菜单Git 分支策略
重构一定要在独立分支上做。推荐用 worktree 隔离,详见 Git 指南:
# 方式一:普通分支
git checkout -b refactor/navigation-split
# 方式二:worktree 隔离(推荐)
git worktree add ../my-blog-refactor -b refactor/navigation-splitCLAUDE.md 重构规则
在开始重构前,给 CLAUDE.md 加上重构专用规则:
## 重构规范
- 每次只做一种类型的重构(不要同时重命名 + 拆分 + 改类型)
- 每步重构后必须通过所有现有测试
- 不改变公共 API 的行为签名
- 提取的工具函数:camelCase,放 lib/;组件:PascalCase,放 components/
- Commit: refactor: 结构调整 / refactor!: 有 breaking change重构前检查清单
| 检查项 | 状态 | 说明 |
|---|---|---|
| 测试覆盖率 ≥ 70% | ⬜ | 重点覆盖要重构的模块 |
| 独立分支已创建 | ⬜ | 不在 main 上直接改 |
| CLAUDE.md 已更新 | ⬜ | 加入重构规范 |
| 当前测试全部通过 | ⬜ | npm test 无失败 |
| 已识别重构范围 | ⬜ | 明确哪些文件会被改动 |
| 已备份未完成的工作 | ⬜ | git stash 保存当前进度 |
三、提取与拆分
提取是最常见的重构手法——大函数拆小函数,大文件拆小模块,臃肿组件拆成职责单一的子组件。
3.1 Extract Function
当一个函数超过 50 行,或者你需要写注释来解释"这一段在干什么",就该提取了:
> 看一下 lib/posts.ts 的 getPostsByLocale 函数,
> 它现在做了太多事(读文件、解析 frontmatter、排序、分页)。
> 帮我把每个职责提取成独立函数,保持原函数作为编排层。// Before: 一个 80 行的大函数
export function getPostsByLocale(locale: Locale, page?: number) {
// 读取文件... 解析 frontmatter... 过滤排序... 分页... 生成元数据...
}
// After: 职责清晰的小函数 + 编排层
function readMdxFiles(locale: Locale): string[] { /* ... */ }
function parseFrontmatter(content: string): PostMeta { /* ... */ }
function sortByDate(posts: PostMeta[]): PostMeta[] { /* ... */ }
function paginate<T>(items: T[], page: number, perPage: number): PaginatedResult<T> { /* ... */ }
export function getPostsByLocale(locale: Locale, page: number = 1) {
const files = readMdxFiles(locale)
const posts = files.map(parseFrontmatter)
const sorted = sortByDate(posts)
return paginate(sorted, page, 10)
}3.2 Extract Module
当一个文件超过 300 行,或者包含多个不相关的 export:
> lib/utils.ts 已经 500 多行了,里面有日期、字符串、URL、数组四类函数。
> 帮我拆成 lib/date.ts、lib/string.ts、lib/url.ts、lib/array.ts。
> 保留 lib/utils.ts 作为统一导出入口(re-export),更新所有 import。关键提示词技巧(更多参考 Prompt 指南):
| 提示词 | 效果 |
|---|---|
| "保留原文件作为 re-export 入口" | 避免破坏现有 import |
| "更新所有 import 路径" | 确保全局一致性 |
| "每个文件只包含相关函数" | 明确拆分标准 |
| "加上 barrel export (index.ts)" | 简化导入路径 |
3.3 Split Component
组件拆分是前端重构的核心。判断标准:超过 200 行、有多个独立 UI 区域、有可复用子模式、状态管理混在渲染逻辑里。
> Navigation.tsx 现在 350 行,包含桌面导航、移动端菜单、主题切换。
> 帮我拆分成:
> - NavBar.tsx:外层容器 + 桌面端链接
> - MobileMenu.tsx:移动端 hamburger 菜单(含动画)
> - NavLink.tsx:单个导航链接(含 active 状态)
> 要求:用 props 传递数据,保持现有 aria 属性和键盘导航。拆分后的文件结构:
components/navigation/
index.ts # re-export Navigation
NavBar.tsx # 主容器(~90 行)
MobileMenu.tsx # 移动端菜单(~80 行)
NavLink.tsx # 单个链接(~25 行)
types.ts # 共享类型
四、重命名与移动
重命名看起来简单,但跨文件重命名是最容易出错的重构之一。
4.1 安全重命名工作流
# 变量/函数重命名
> 把 lib/posts.ts 里的 getAllPosts 重命名为 getPostsByLocale。
> 更新所有引用(import、调用、测试、类型定义),列出所有被修改的文件。
# 文件重命名
> 把 components/Nav.tsx 重命名为 components/Navigation.tsx,更新所有 import。4.2 跨文件重命名策略
涉及大量文件时,分步更安全:
# Step 1: 分析影响范围(先不改)
> 我想把 PostMeta 类型重命名为 PostFrontmatter。
> 先不要改,只告诉我这个类型在哪些文件中被使用,预计修改多少个文件。
# Step 2: 确认后执行
> 确认修改,每个文件改完后跑一次类型检查。
# Step 3: 验证
> 跑一下 tsc --noEmit 和 npm test,确认没有遗漏。4.3 常见陷阱
| 陷阱 | 原因 | 解决方案 |
|---|---|---|
| 动态 import 没更新 | import() 路径是字符串 | 提示检查动态 import |
| CSS module 引用断裂 | 样式文件没跟着移动 | 要求"连同样式文件一起移动" |
| 测试文件路径没更新 | 测试里的 import 被遗漏 | 要求"包括 __tests__ 和 *.test.*" |
| barrel export 没更新 | index.ts 的 re-export 过时 | 要求"更新所有 index.ts" |
| 路径别名失效 | tsconfig.json paths 没更新 | 要求"检查 tsconfig paths" |
五、类型系统重构
类型重构是 TypeScript 项目中最有价值的重构之一。
5.1 JavaScript → TypeScript 渐进迁移
不要一次性迁移。让 Claude Code 按依赖顺序制定计划:配置 → 类型定义 → 工具函数(叶子节点)→ 数据层 → 组件(从小到大)→ 页面。关键是 allowJs: true 让 JS/TS 共存。
5.2 消除 any 类型
> 搜索项目中所有 any 类型,按分类统计(参数、返回值、变量、as any),
> 给每个 any 建议具体的替代类型。// ❌ Before
function processData(data: any): any {
const result: any = {}
data.items.forEach((item: any) => { result[item.id] = item.value })
return result
}
// ✅ After
interface DataItem { id: string; value: number }
interface DataInput { items: DataItem[] }
type ProcessedData = Record<string, number>
function processData(data: DataInput): ProcessedData {
const result: ProcessedData = {}
data.items.forEach((item) => { result[item.id] = item.value })
return result
}5.3 Union → Discriminated Union
当你有一个 union 类型需要在运行时区分,discriminated union 是最安全的模式:
> 把 Notification 类型从普通 union 重构为 discriminated union,
> 按 type 字段区分,每种通知类型只包含它需要的字段。// ❌ Before: 所有字段都是可选的
type Notification = {
type: string; title: string; message?: string; progress?: number; error?: Error
}
// ✅ After: 每种类型精确定义
type Notification =
| { type: 'info'; title: string; message: string }
| { type: 'success'; title: string; message: string }
| { type: 'progress'; title: string; progress: number }
| { type: 'error'; title: string; error: Error }
// 类型收窄自动生效
function renderNotification(n: Notification) {
switch (n.type) {
case 'progress': return <ProgressBar value={n.progress} /> // TS 知道 progress 存在
case 'error': return <ErrorMessage error={n.error} /> // TS 知道 error 存在
}
}5.4 泛型重构
当多个函数有相似模式,泛型可以消除重复:
// ❌ Before: 三个几乎一样的函数
async function fetchUsers(): Promise<ApiResponse<User[]>> {
const res = await fetch('/api/users')
if (!res.ok) throw new ApiError(res.status)
return res.json()
}
// fetchPosts, fetchComments 也是一样的模式...
// ✅ After: 一个泛型函数
async function fetchResource<T>(endpoint: string): Promise<ApiResponse<T>> {
const res = await fetch(`/api/${endpoint}`)
if (!res.ok) throw new ApiError(res.status)
return res.json()
}
const users = await fetchResource<User[]>('users') // 类型安全六、API 与接口重构
API 重构是最需要谨慎的类型,因为它影响的不只是你的代码,还有所有调用方。
6.1 Breaking Change 管理策略
| 策略 | 适用场景 | 风险 | 实施成本 |
|---|---|---|---|
| 直接修改 | 内部 API,调用方可控 | 低 | 低 |
| 新旧并存 + 废弃标记 | 公共 API,需要过渡期 | 低 | 中 |
| Adapter 模式 | 接口变化大,但逻辑不变 | 低 | 中 |
| 版本化 API | REST API,多版本共存 | 中 | 高 |
| Feature Flag | 渐进式切换 | 低 | 中 |
6.2 Adapter 模式过渡
当内部实现大改,但需要保持旧接口兼容时:
> getPostsByLocale 的返回值结构要改:
> 旧:{ posts: Post[], total: number }
> 新:{ data: Post[], meta: { total, page, perPage } }
> 帮我:创建新实现 V2,写 adapter 让旧函数调用新实现,加 @deprecated 标记。// 新实现
function getPostsByLocaleV2(locale: Locale, options: QueryOptions): PaginatedResult<Post> {
return { data: posts, meta: { total, page: options.page, perPage: options.perPage } }
}
// Adapter:旧接口 → 新实现
/** @deprecated 请使用 getPostsByLocaleV2,此函数将在 v3.0 移除 */
function getPostsByLocale(locale: Locale, page?: number) {
const result = getPostsByLocaleV2(locale, { page: page ?? 1, perPage: 10 })
return { posts: result.data, total: result.meta.total }
}6.3 Deprecation + 迁移指南
让 Claude Code 给旧函数加 @deprecated JSDoc(含迁移代码示例),开发环境输出 console.warn,并生成 MIGRATION.md:
/**
* @deprecated 自 v2.5.0 起废弃,将在 v3.0.0 移除。
* 请使用 `formatDateTime(new Date('2026-03-21'), { style: 'short' })` 替代。
*/
function formatDate(date: string): string {
if (process.env.NODE_ENV === 'development') {
console.warn('[DEPRECATED] formatDate() → 请使用 formatDateTime()')
}
return formatDateTime(new Date(date), { style: 'short' })
}七、大规模重构策略
当重构涉及几十甚至上百个文件时,需要更系统的策略。
7.1 分步迁移
大规模重构最忌讳一步到位。拆成小步,每步可验证:
# Step 1: 分析(Plan Mode)
> /plan 我要把状态管理从 Redux 迁移到 Zustand。
> 分析有多少 slice、多少组件用了 useSelector/useDispatch、有没有 middleware。
> 给我一个分步迁移计划。
# Step 2: 按模块逐个迁移
> 先迁移 userSlice:创建 Zustand store → 更新组件 → 跑测试 → 提交
# Step 3: 重复直到完成
> 继续迁移 postSlice...7.2 多 Agent 并行重构
对于互不依赖的模块,用多个 Claude Code 实例并行。详见多 Agent 指南:
# 终端 1: 重构用户模块
claude "把 stores/userSlice.ts 从 Redux 迁移到 Zustand,更新所有相关组件和测试"
# 终端 2: 重构文章模块
claude "把 stores/postSlice.ts 从 Redux 迁移到 Zustand,更新所有相关组件和测试"
# 终端 3: 重构评论模块
claude "把 stores/commentSlice.ts 从 Redux 迁移到 Zustand,更新所有相关组件和测试"注意:确保并行模块之间没有直接依赖,共享类型定义的修改要放在最前面单独完成。
7.3 Worktree 隔离
风险较高的重构,用 Git worktree 隔离最安全(详见 Git 指南)。主工作区不受影响,失败直接删除 worktree 零成本:
git worktree add ../blog-refactor-redux -b refactor/redux-to-zustand
cd ../blog-refactor-redux && claude # 在隔离工作区中重构7.4 进度追踪
# Redux → Zustand 迁移进度
| 模块 | 状态 | 组件数 | 测试 | 备注 |
|------|------|--------|------|------|
| userSlice | ✅ 已验证 | 8 | 通过 | - |
| postSlice | 🔄 进行中 | 12 | - | 有 middleware |
| commentSlice | ⬜ 待开始 | 5 | - | - |
| authSlice | ⬜ 待开始 | 6 | - | 依赖 userSlice |
| uiSlice | ⬜ 待开始 | 15 | - | 最多组件 |八、重构安全网
重构最怕"改完了不知道有没有破坏什么"。安全网就是你的信心来源。
8.1 测试驱动重构
最可靠的安全网。详细 TDD 工作流参考测试指南:
# 重构前:确保测试通过
> 跑一下 Navigation 组件的所有测试,确认当前是绿的。
# 重构中:每步验证
> 我刚把 MobileMenu 提取成独立组件了,跑一下测试。
# 重构后:补充新测试
> 重构完成,帮我补充 NavBar、MobileMenu、NavLink 的独立测试。8.2 快照对比
对于 UI 组件重构,快照测试能快速发现渲染差异:
describe('Navigation refactoring', () => {
it('should render identically after refactoring', () => {
const { container } = render(<Navigation locale="zh" pathname="/zh/blog" />)
expect(container).toMatchSnapshot()
})
})8.3 回滚策略
git stash push -m "refactor attempt 1" # 小改动:stash
git checkout main && git branch -D refactor/xxx # 大改动:删分支重来
git reset --soft HEAD~3 # 逐 commit 回退,保留改动8.4 重构后验证清单
| 验证项 | 命令 | 通过标准 |
|---|---|---|
| TypeScript 编译 | tsc --noEmit | 零错误 |
| 单元测试 | npm test | 全部通过 |
| Lint 检查 | npm run lint | 零错误 |
| 构建测试 | npm run build | 构建成功 |
| 快照对比 | npm test -- -u | 差异已确认 |
| 性能对比 | Lighthouse | 无明显退化 |
| Bundle 大小 | npm run analyze | 无明显增长 |
> 重构完成了,帮我跑一遍验证清单:tsc --noEmit → npm test → npm run lint → npm run build
> 每步结果告诉我,有问题就停下来。九、CLAUDE.md 重构规范
好的 CLAUDE.md 配置能让 Claude Code 在重构时自动遵循团队规范。完整用法参考 CLAUDE.md 指南。
9.1 命名规范
## 命名规范
- 文件:组件 PascalCase / 工具函数 camelCase / 测试 同名.test
- 变量:组件 PascalCase / 函数 camelCase / 常量 UPPER_SNAKE_CASE
- 类型:PascalCase / 泛型 T, TData / Hook useCamelCase
- 布尔:is/has/should 前缀(isOpen, hasError)9.2 文件组织规范
## 文件组织
- 组件拆分后放同名目录:components/navigation/NavBar.tsx
- 每个组件目录必须有 index.ts 统一导出
- 共享类型放 types.ts,私有类型就近定义
- 文件大小限制:组件 250 行 / 工具函数 200 行 / 类型 150 行
- Import 顺序:React 内置 → 第三方 → 内部模块 → 相对路径 → 类型9.3 重构 Commit 规范
## 重构 Commit
- refactor: 不改变行为的结构调整
- refactor!: 有 breaking change(需要 BREAKING CHANGE footer)
- 每个独立重构步骤一个 commit,不混合"提取"和"重命名"
- Message 格式:refactor(scope): 简短描述 + 详细说明为什么重构9.4 完整 CLAUDE.md 重构配置
# 重构规范
## 原则
- 小步重构,每步可验证
- 不改变公共 API 的外部行为
- 每步重构后必须通过所有测试
- 重构和功能修改不混在同一个 commit
## 流程
1. 确认测试覆盖率 ≥ 70%
2. 创建重构分支
3. 每步重构后跑 tsc + test
4. 用 /review 检查改动
5. 按步骤提交(每步一个 commit)
## 禁止事项
- 不要在重构 commit 中加新功能
- 不要删除测试(可以移动或重命名)
- 不要改变公共函数的签名(除非是 refactor!)
- 不要一次性重构超过 10 个文件(拆成多个 PR)十、完整实战:重构 Navigation 组件
理论讲完了,来一个完整实战。把博客的 Navigation 组件从单文件拆分成多个子组件。
10.1 现状分析
当前 components/Navigation.tsx 有 350 行,问题很明显:
// components/Navigation.tsx — 350 行的单文件组件
'use client'
import { usePathname } from 'next/navigation'
import { useTranslations } from 'next-intl'
import { useState, useEffect, useRef } from 'react'
export default function Navigation({ locale }: { locale: Locale }) {
const [isMenuOpen, setIsMenuOpen] = useState(false)
// Escape 键关闭菜单 (20 行)
// 点击外部关闭菜单 (15 行)
// 自动聚焦第一个链接 (10 行)
// 渲染桌面导航 (40 行)
// 渲染移动端 hamburger (20 行)
// 渲染移动端菜单面板 (60 行)
// 渲染主题切换 + 语言切换 (30 行)
return <nav>{/* 350 行的 JSX */}</nav>
}桌面端和移动端逻辑混在一起,三个 useEffect 管理菜单状态,单个链接的 active 逻辑重复出现,难以单独测试。
10.2 Step 1:分析并制定计划
> 分析 components/Navigation.tsx,我想拆分成更小的组件。
> 当前有哪些职责混在一起?建议拆成哪几个?Props 接口?状态放哪层?
> 先不要改代码。Claude Code 分析:拆成 NavBar(主容器 + 状态管理)、NavLink(active 判断 + 样式)、MobileMenu(开关 + 动画 + 焦点管理)。isMenuOpen 放在 NavBar 层,pathname 在 NavBar 获取后传给子组件。
10.3 Step 2:先写测试
> 在拆分前,帮我写测试覆盖当前行为:渲染所有链接、aria-current、hamburger 开关、Escape 关闭。describe('Navigation', () => {
it('renders all nav links', () => {
render(<Navigation locale="zh" />)
expect(screen.getByText('home')).toBeInTheDocument()
expect(screen.getByText('blog')).toBeInTheDocument()
})
it('marks current page link as active', () => {
render(<Navigation locale="zh" />)
expect(screen.getByText('blog').closest('a')).toHaveAttribute('aria-current', 'page')
})
it('toggles mobile menu', () => {
render(<Navigation locale="zh" />)
fireEvent.click(screen.getByRole('button', { name: /openMenu/i }))
expect(screen.getByRole('navigation')).toHaveClass('menu-open')
fireEvent.keyDown(document, { key: 'Escape' })
expect(screen.getByRole('navigation')).not.toHaveClass('menu-open')
})
})10.4 Step 3:提取 NavLink
从最简单的开始:
> 从 Navigation.tsx 中提取 NavLink 组件到 components/navigation/NavLink.tsx。
> Props: { href, label, isActive },包含 active 样式和 aria-current。
> 在 Navigation.tsx 中使用新组件,跑测试确认。// components/navigation/NavLink.tsx
import { Link } from '@/i18n/navigation'
interface NavLinkProps { href: string; label: string; isActive: boolean }
export default function NavLink({ href, label, isActive }: NavLinkProps) {
return (
<Link
href={href}
className={`nav-link ${isActive ? 'nav-link-active' : ''}`}
aria-current={isActive ? 'page' : undefined}
>
{label}
</Link>
)
}测试通过,Navigation.tsx 从 350 行减少到 310 行。提交:
> 提交:refactor(navigation): extract NavLink into separate component10.5 Step 4:提取 MobileMenu
最复杂的一步,涉及状态管理和焦点控制:
> 从 Navigation.tsx 中提取 MobileMenu 组件:
> Props: { items, isOpen, onToggle, onClose, pathname }
> 包含 Escape 关闭、自动聚焦的 useEffect,使用 NavLink 渲染链接。
> 保持所有 aria 属性和键盘导航。// components/navigation/MobileMenu.tsx
'use client'
import { useEffect, useRef } from 'react'
import { useTranslations } from 'next-intl'
import NavLink from './NavLink'
import type { NavItem } from './types'
interface MobileMenuProps {
items: NavItem[]; isOpen: boolean
onToggle: () => void; onClose: () => void; pathname: string
}
export default function MobileMenu({ items, isOpen, onToggle, onClose, pathname }: MobileMenuProps) {
const t = useTranslations('a11y')
const firstLinkRef = useRef<HTMLAnchorElement>(null)
// Escape 键关闭
useEffect(() => {
if (!isOpen) return
const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose() }
document.addEventListener('keydown', handler)
return () => document.removeEventListener('keydown', handler)
}, [isOpen, onClose])
// 自动聚焦
useEffect(() => { if (isOpen) firstLinkRef.current?.focus() }, [isOpen])
return (
<div className="md:hidden">
<button onClick={onToggle} aria-label={isOpen ? t('closeMenu') : t('openMenu')}
aria-expanded={isOpen} type="button">{/* hamburger icon */}</button>
{isOpen && (
<div className="mobile-menu" role="menu">
{items.map((item) => (
<NavLink key={item.href} href={item.href} label={item.label}
isActive={pathname.includes(item.href)} />
))}
</div>
)}
</div>
)
}测试通过,Navigation.tsx 从 310 行减少到 180 行。提交。
10.6 Step 5:整理 NavBar 主组件
> 最后一步:整理成干净的 NavBar,创建 index.ts 和 types.ts,更新所有 import。// components/navigation/types.ts
export interface NavItem { href: string; label: string }
// components/navigation/NavBar.tsx — 整理后的主组件(~90 行)
'use client'
import { usePathname } from 'next/navigation'
import { useTranslations } from 'next-intl'
import { useState, useCallback } from 'react'
import NavLink from './NavLink'
import MobileMenu from './MobileMenu'
import ThemeToggle from '../ThemeToggle'
export default function NavBar({ locale }: { locale: Locale }) {
const t = useTranslations('nav')
const pathname = usePathname()
const [isMenuOpen, setIsMenuOpen] = useState(false)
const navItems: NavItem[] = [
{ href: '/', label: t('home') }, { href: '/blog', label: t('blog') },
{ href: '/projects', label: t('projects') }, { href: '/about', label: t('about') },
]
const isActive = (href: string) =>
href === '/' ? pathname === `/${locale}` : pathname.startsWith(`/${locale}${href}`)
return (
<nav className={`navbar ${isMenuOpen ? 'menu-open' : ''}`}>
<div className="hidden md:flex items-center gap-6">
{navItems.map(item => (
<NavLink key={item.href} href={item.href} label={item.label} isActive={isActive(item.href)} />
))}
</div>
<MobileMenu items={navItems} isOpen={isMenuOpen}
onToggle={useCallback(() => setIsMenuOpen(p => !p), [])}
onClose={useCallback(() => setIsMenuOpen(false), [])} pathname={pathname} />
<div className="flex items-center gap-2"><ThemeToggle /></div>
</nav>
)
}
// components/navigation/index.ts
export { default as Navigation } from './NavBar'
export { default as NavLink } from './NavLink'
export { default as MobileMenu } from './MobileMenu'10.7 Step 6:补充子组件测试
> 给拆分后的组件补充独立测试:NavLink、MobileMenu、NavBar 集成测试。describe('NavLink', () => {
it('applies active styles when isActive', () => {
render(<NavLink href="/blog" label="Blog" isActive={true} />)
expect(screen.getByText('Blog').closest('a')).toHaveAttribute('aria-current', 'page')
})
})
describe('MobileMenu', () => {
it('calls onClose when Escape is pressed', () => {
const onClose = vi.fn()
render(<MobileMenu items={[{ href: '/', label: 'Home' }]}
isOpen={true} onToggle={vi.fn()} onClose={onClose} pathname="/zh" />)
fireEvent.keyDown(document, { key: 'Escape' })
expect(onClose).toHaveBeenCalled()
})
})10.8 最终验证
> 跑完整验证:tsc --noEmit → npm test → npm run lint → npm run build
# ✓ tsc --noEmit — 零错误
# ✓ npm test — 24 tests passed(原 17 + 新增 7)
# ✓ npm run lint — 零警告
# ✓ npm run build — 构建成功最终 commit 历史:
git log --oneline -5
# a1b2c3d refactor(navigation): add unit tests for NavLink and MobileMenu
# d4e5f6g refactor(navigation): reorganize NavBar as orchestration component
# h7i8j9k refactor(navigation): extract MobileMenu into separate component
# l0m1n2o refactor(navigation): extract NavLink into separate component
# p3q4r5s test(navigation): add baseline tests before refactoring重构前后对比
| 指标 | 重构前 | 重构后 |
|---|---|---|
| 文件数 | 1 个(350 行) | 5 个(总 ~280 行) |
| 最大文件 | 350 行 | 90 行(NavBar) |
| 测试数 | 0 | 11 |
| 测试覆盖率 | 0% | 85% |
| 可复用组件 | 0 | 2(NavLink, MobileMenu) |
| 职责清晰度 | 混合 | 单一职责 |
这就是完整的 Claude Code 重构工作流:分析 → 写测试 → 小步拆分 → 逐步验证 → 补充测试 → 最终确认。
总结
重构不是一次性的大工程,而是持续的小改进。Claude Code 让重构的成本大幅降低——以前需要半天的提取和重命名,现在几分钟就能完成,而且更安全。
关键心法:永远先有测试再动手重构;小步前进每步可验证;用 CLAUDE.md 固化规范;大规模重构拆成多个 PR;重构和功能开发分开提交。
配合调试指南一起使用——调试解决"坏了怎么修",重构解决"没坏但可以更好"。
推荐阅读
- Claude Code 进阶指南 — 基础重构流程和 Plan Mode 用法
- Claude Code 调试工作流指南 — 姊妹篇,系统性调试方法论
- Claude Code 测试工作流指南 — TDD、Coverage、测试驱动重构的基础
- Claude Code Git 工作流指南 — Worktree 隔离、分支策略、Commit 规范
- Claude Code Prompt 技巧指南 — 写出高质量重构提示词的方法
- Claude Code 多 Agent 指南 — 并行重构的多 Agent 策略
- CLAUDE.md 指南 — 完整的 CLAUDE.md 配置方法
- Claude Code Slash Commands 指南 — /review 命令在重构中的应用
- Claude Code 上下文管理指南 — 大规模重构时的上下文控制
- Claude Code Hooks 指南 — 自动化重构验证的 Hook 配置