引言:为什么遗留代码不是死刑
每个开发者迟早都会遇到遗留的 JavaScript 代码。可能是 5 年前"随手"编写的单体 Node.js 脚本,或是连高级开发人员都不敢触碰的混乱回调函数。然而,重构遗留代码不仅仅是"从头重写所有内容",而是一项在不改变外部行为的前提下改善架构的系统性工作。
在本文中,我们将详细讲解 JavaScript(Node.js)的分步重构计划:从分析和测试到实现现代模式。您将学习如何使用实际示例,安全地将"烂摊子"转变为可维护的代码。
1. 遗留代码的分析与映射
在修改代码之前,需要了解其当前状态。首先创建依赖关系图并识别"热点区域"。
1.1. 分析工具
对于 JavaScript/Node.js,使用以下工具:
- ESLint 配合
complexity和max-lines规则——用于识别复杂函数。 - Plato 或 CodeClimate——用于可视化圈复杂度。
- Madge——用于构建模块依赖关系图。
1.2. 识别"代码坏味"
典型的遗留 JS 问题:
- 超过 100 行的函数。
- 全局变量和状态突变。
- 回调地狱(嵌套的异步调用)。
- 缺乏模块化(单个文件超过 2000 行)。
// "代码坏味"示例:一个包揽所有事情的函数function processUserData(userId) { // 100 行:验证、数据库查询、格式化、发送邮件 // ... 难以测试和维护}2. 构建"安全网":测试
没有测试,重构就是"俄罗斯轮盘赌"。创建表征测试(捕捉当前行为的测试)。
2.1. 添加单元测试
使用 Jest 或 Mocha。从关键路径开始:
// 测试现有函数,不修改它const { processUserData } = require('./legacy');
describe('processUserData (遗留)', () => { it('应处理 ID 为 1 的用户', () => { const result = processUserData(1); expect(result).toHaveProperty('email'); expect(result.status).toBe('processed'); });});2.2. Node.js 的集成测试
如果代码与数据库或 API 交互,使用 supertest 进行 HTTP 请求:
const request = require('supertest');const app = require('../app');
describe('POST /api/users', () => { it('应创建用户(遗留行为)', async () => { const res = await request(app) .post('/api/users') .send({ name: 'Test' }); expect(res.status).toBe(200); expect(res.body.id).toBeDefined(); });});3. 逐步重构:从单体到模块
我们将过程分为 3 个阶段:隔离、分解、优化。
3.1. 隔离与包装
不要触及内部实现——将遗留函数包装在新的接口中:
// 遗留函数function oldFormatDate(date) { // ... 50 行包含突变的代码 return formattedDate;}
// 新包装器function formatDateSafe(date) { if (!(date instanceof Date)) { throw new Error('无效日期'); } return oldFormatDate(date);}3.2. 使用提取函数进行分解
将逻辑块提取为独立的函数:
// 之前:一个 80 行的函数function handleOrder(order) { // 1. 验证 if (!order.id || !order.total) { ... } // 2. 折扣计算