前端工具函数治理实战:从混乱到规范的 AI 驱动重构之路

基于 TDD + 向后兼容 + 文档化的渐进式重构方案

作者: CMC Link IBS Web 团队

标签: Vue3 TypeScript 工程化 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)]  // 😱
}

问题清单:

  • 同一功能多处实现 (uuid vs guid)
  • 工具函数分散在多个文件
  • 已安装 lodash-es 但很少使用
  • @ts-ignoreas 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 替换为 nanoid45分钟10分钟78%
12个函数 JSDoc6小时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 量化成果

指标治理前治理后
测试用例0169
@ts-ignore40
重复实现2 组0
文档完整度部分100%
ESLint 规则01

6.2 经验教训

  1. AI 是结对伙伴,不是替代者 - 人工审核不可缺
  2. 测试先行仍是核心 - AI 生成的测试也需人工审核业务逻辑
  3. 向后兼容很重要 - 不能影响现有业务
  4. Prompt 库是团队资产 - 可复用、可迭代
  5. 拥抱开源库 - 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)同上
toCamelCasetoCamelCase (lodash-es)基于 lodash

C. AI Prompt 模板

详见 utils-governance-issues.md


结语

工具函数治理不是一次性的工作,而是持续的过程。通过 AI 驱动 + TDD + 渐进式迁移的方式,我们在 不影响业务 的前提下,将测试覆盖率从 0 提升到 169 个用例,消除了所有 @ts-ignore,建立了完整的文档体系。

希望这个方法论能帮助到有类似需求的团队。欢迎在评论区交流!


相关链接: