基于 TDD + 向后兼容 + 文档化的渐进式重构方案
作者: CMC Link IBS Web 团队
标签:
Vue3TypeScript工程化AI辅助开发代码治理
前言
2025 年了,谁还在人肉写单测?
在一个 900+ 文件的 Vue 3 + TypeScript 企业级项目中,我们遇到了典型的工具函数混乱问题。本文将分享我们如何通过 AI 驱动 + TDD + 渐进式迁移 的方式,在 不影响业务的前提下,完成工具函数的全面治理。
治理成果预览:
- 测试用例: 0 → 169 个
- @ts-ignore: 4 → 0
- 治理时间: 传统方式 ~15小时 → AI 驱动 ~1.5小时
第一章:问题背景
1.1 项目现状
CMC Link IBS Web 是一个集装箱运输业务管理系统:
- 代码规模: 567 个 TypeScript 文件 + 373 个 Vue 组件
- 业务模块: 船期管理、订舱(SO)、提单(SI/BL)、VGM、设备控制
- 技术栈: Vue 3 + Pinia + Element Plus + TailwindCSS
1.2 工具函数的痛点
在一次代码审查中,我们发现:
// src/common/utils.ts - 4 个 @ts-ignore
export function uuid(len: number, radix?: number) {
// @ts-ignore
uuid[i] = chars[0 | (Math.random() * radix)] // 😱
}
问题清单:
- 同一功能多处实现 (
uuidvsguid) - 工具函数分散在多个文件
- 已安装 lodash-es 但很少使用
@ts-ignore和as any绕过类型检查- 没有一个测试
1.3 业务领域的特殊性
集运业务系统有其独特需求:
- 树形结构频繁: 权限菜单、角色层级
- 时间处理核心: 船期、截关时间、多时区
- 数据转换多: 提单字段大写 (SI/BL)
使用场景 | 工具函数 | 使用模块
---------------------|---------------------|------------------
权限菜单构建 | arrayToTree | CachePermissions
提单数据大写转换 | deepToLocaleUpperCase | SI补料, Manifest
路由转驼峰命名 | toCamelCase | 路由注册
唯一ID生成 | uuid/guid | 表格行Key
第二章:治理原则
2.1 测试驱动开发 (TDD)
为什么先写测试?
// 锁定现有行为,确保重构不破坏功能
describe('arrayToTree - 现有行为基线', () => {
it('应与新实现行为一致', () => {
const oldResult = oldArrayToTree(testData)
const newResult = newArrayToTree(testData)
expect(newResult).toEqual(oldResult) // 关键: I/O 一致性
})
})
2.2 向后兼容
核心策略: 只标记废弃,不删除代码
/**
* @deprecated 请使用 `@/utils/crypto/id` 中的 generateUUID
*/
export function uuid(len: number): string {
return generateRandomString(len) // 内部调用新实现
}
2.3 文档化
每个函数都是 API,必须有完整文档:
/**
* 生成唯一 ID
* @param size - ID 长度,默认 21 位
* @returns 生成的唯一 ID
* @example
* generateId() // => 'V1StGXR8_Z5jdHi6B-myT'
* generateId(12) // => 'IRFa-VaY2b'
*/
export function generateId(size: number = 21): string
第三章:AI 驱动的代码治理
"2025 年了,谁还在人肉写单测?"
3.1 AI 结对编程工作流
传统方式: 人工写测试 → 人工实现 → 人工调试 → 人工写文档
AI 驱动: AI 生成测试 → 人工审核 → AI 实现 → AI 文档 → 人工验证
3.2 实测效率对比
| 任务 | 传统方式 | AI 驱动 | 效率提升 |
|---|---|---|---|
| arrayToTree 单测 | 2小时 | 8分钟 | 93% |
| toCamelCase 重构 | 1小时 | 12分钟 | 80% |
| uuid 替换为 nanoid | 45分钟 | 10分钟 | 78% |
| 12个函数 JSDoc | 6小时 | 30分钟 | 92% |
| 总计 | ~15小时 | ~1.5小时 | 90% |
3.3 具体 Prompt 示例
测试生成 Prompt:
为以下函数编写 vitest 单元测试:
- 覆盖正常/边界/异常情况
- 使用中文描述 describe/it
- 包含快照测试
- 考虑业务场景: 权限菜单/提单数据
[贴入函数代码]
重构 Prompt:
重构此函数:
- 使用 lodash-es 替代自实现
- 添加完整 TypeScript 泛型
- 消除 @ts-ignore 和 as any
- 确保 I/O 与原函数一致
[贴入函数代码]
3.4 AI 质量保障机制
AI 输出不是终点,人工审核是关键:
AI 输出 → 人工审核 → 运行测试 → 代码审查 → 合入主干
↑ ↓
└── 修正问题 ─┘
审核检查清单:
- 测试用例是否覆盖业务场景?
- 边界情况是否完整?
- 重构后是否通过所有测试?
- JSDoc 示例是否正确?
第四章:实施过程
4.1 Phase 1: 测试基础建设 (Week 1)
目标: 为所有现有工具函数补充单测
// src/utils/__tests__/utils.test.ts
describe('arrayToTree', () => {
describe('基本功能', () => {
it('应将扁平数据转换为树形结构', () => {
const data = [
{ id: '1', parentId: null, name: '根节点' },
{ id: '2', parentId: '1', name: '子节点1' },
]
const result = arrayToTree(data)
expect(result).toHaveLength(1)
expect(result[0].children).toHaveLength(1)
})
})
describe('业务场景: 权限菜单构建', () => {
it('应正确构建多级菜单树', () => {
// 真实业务数据测试
})
})
})
成果: 创建 147 个测试用例,发现 2 个隐藏 bug
4.2 Phase 2: 新函数开发 (Week 2-3)
使用 nanoid 替代 uuid:
// src/utils/crypto/id.ts
import { customAlphabet, nanoid } from 'nanoid'
export function generateId(size: number = 21): string {
return nanoid(size)
}
export function generateUUID(): string {
return crypto.randomUUID()
}
使用 lodash-es 重构 toCamelCase:
// src/utils/string/case.ts
import { camelCase, upperFirst } from 'lodash-es'
export function toCamelCase(str: string, upperCaseFirst = false): string {
if (!str || typeof str !== 'string') return ''
const result = camelCase(str)
return upperCaseFirst ? upperFirst(result) : result
}
4.3 Phase 3: 废弃标记 (Week 4)
添加 @deprecated:
/**
* @deprecated 请使用 `@/utils/crypto/id` 中的新实现
*
* 迁移指南:
* ```typescript
* // 旧写法
* import { uuid } from '~/common/utils'
* // 新写法
* import { generateId } from '~/utils'
* ```
*/
export function uuid(len: number): string {
return generateRandomString(len)
}
配置 ESLint 规则:
// eslint.config.js
{
group: ['@/common/utils', '~/common/utils'],
message: '🔄 迁移提示:请从 ~/utils 导入'
}
第五章:核心函数治理示例
5.1 arrayToTree - 保留并优化
分析: 现有实现已是 O(n) 复杂度,使用 Map 优化,无需替换
治理内容:
- ✅ 补充完整单元测试 (15 个用例)
- ✅ 添加 JSDoc 文档
- ⏳ 添加泛型支持 (后续)
5.2 deepToLocaleUpperCase - 发现并修复 bug
发现的 bug: 递归调用时未传递 config 参数
// 修复前 ❌
return obj.map(item => deepToLocaleUpperCase(item))
// 修复后 ✅
return obj.map(item => deepToLocaleUpperCase(item, config))
影响: SI/BL 提单数据大写转换,嵌套对象不会应用自定义配置
5.3 uuid/guid - 替换为 nanoid
替换理由:
- 消除 4 个 @ts-ignore
- nanoid 比 UUID v4 更短 (21 位 vs 36 位)
- 相同的碰撞概率,更好的性能
// 旧实现: 4 个 @ts-ignore, 手动拼接
export function uuid(len: number) {
// @ts-ignore
uuid[i] = chars[0 | (Math.random() * radix)]
}
// 新实现: 类型安全,基于 nanoid
export function generateId(size = 21): string {
return nanoid(size)
}
第六章:成果与经验
6.1 量化成果
| 指标 | 治理前 | 治理后 |
|---|---|---|
| 测试用例 | 0 | 169 |
| @ts-ignore | 4 | 0 |
| 重复实现 | 2 组 | 0 |
| 文档完整度 | 部分 | 100% |
| ESLint 规则 | 0 | 1 |
6.2 经验教训
- AI 是结对伙伴,不是替代者 - 人工审核不可缺
- 测试先行仍是核心 - AI 生成的测试也需人工审核业务逻辑
- 向后兼容很重要 - 不能影响现有业务
- Prompt 库是团队资产 - 可复用、可迭代
- 拥抱开源库 - lodash-es/dayjs/nanoid 都是优秀选择
6.3 可复制的方法论
1. 统计现状 → 2. 分类分析 → 3. 制定策略
↓
4. 补充测试 → 5. 实现新函数 → 6. 废弃标记
↓
7. ESLint 规则 → 8. 文档化 → 9. 渐进迁移
附录
A. 目录结构
src/utils/
├── crypto/
│ ├── id.ts # nanoid 封装
│ └── __tests__/id.test.ts
├── string/
│ ├── case.ts # lodash-es 封装
│ └── __tests__/case.test.ts
├── __tests__/utils.test.ts
└── index.ts # 统一导出入口
B. 函数映射表
| 旧函数 | 新函数 | 说明 |
|---|---|---|
uuid(len) | generateRandomString(len) | 生成随机字符串 |
uuid(0) | generateUUID() | 生成 UUID v4 |
guid(len) | generateRandomString(len) | 同上 |
toCamelCase | toCamelCase (lodash-es) | 基于 lodash |
C. AI Prompt 模板
结语
工具函数治理不是一次性的工作,而是持续的过程。通过 AI 驱动 + TDD + 渐进式迁移的方式,我们在 不影响业务 的前提下,将测试覆盖率从 0 提升到 169 个用例,消除了所有 @ts-ignore,建立了完整的文档体系。
希望这个方法论能帮助到有类似需求的团队。欢迎在评论区交流!
相关链接: