接手3年前的前端老项目,这3个“屎山”代码重构技巧,让我少加了10天班

0 阅读7分钟

三大“屎山”代码重构技巧

技巧一:“考古式”重构 —— 先画图、后动刀

核心思想:面对陌生的老代码,不要急着删改,先把自己变成“代码考古学家”,理清业务逻辑。

具体做法

1. 建立“代码地图”

打开项目后,不要直接跳进那个2000行的“怪物组件”。先用思维导图工具(Xmind、ProcessOn)或白板,把代码里的逻辑分支画出来。

操作步骤

  • 从入口文件开始,逐行阅读
  • 遇到一个条件分支(if/else),就在图上画一个分叉
  • 遇到一个API调用,记下接口地址和参数
  • 遇到一个状态变更,追踪它的流向

实战示例
假设你接手的是一个用户管理组件,阅读代码后画出这样的图:

用户管理组件
├── 初始化
│   ├── 获取用户列表 (API: /api/users)
│   ├── 获取角色列表 (API: /api/roles)
│   └── 获取权限配置 (API: /api/permissions)
├── 交互逻辑
│   ├── 搜索
│   │   ├── 按姓名搜索 → 调用搜索接口
│   │   └── 按角色筛选 → 前端过滤
│   ├── 新增用户
│   │   ├── 弹窗表单
│   │   ├── 表单校验
│   │   └── 提交接口
│   └── 编辑用户
│       ├── 权限判断 (admin可编辑)
│       ├── 回填表单
│       └── 提交更新
└── 异常处理
    ├── 网络错误 → 显示重试按钮
    ├── 无权限 → 隐藏操作按钮
    └── 数据为空 → 显示空状态
2. 标记“可疑地带”

在代码中埋下注释标记,比如:

// TODO[重构笔记]: 这里发了3个请求,但只有最后一个数据被使用
// TODO[重构笔记]: 这个函数长达200行,做了数据获取、格式化、权限判断
// TODO[重构笔记]: 这个变量名 'data' 到底存的是什么?看了10分钟没看懂
3. 写“考古报告”

在动手重构前,先写一篇简短的文档,回答三个问题:

  • 这个组件/模块到底是干什么的?
  • 它有哪些业务场景?
  • 目前的代码结构有哪些问题?

为什么要先画图?
因为看不懂就不能重构。很多重构失败的案例,都是因为没搞懂业务逻辑,改完代码后发现功能不全了。画图的过程,就是你把业务逻辑从代码里“提取”出来的过程。


技巧二:“外科手术式”重构 —— 一次只动一个点

核心思想:重构不是重写,要像做外科手术一样精准、微创,每次改动只解决一个问题。

具体做法

1. 制定“手术方案”

把一个大问题拆解成多个小任务。比如那个2000行的组件,可以拆成这样:

手术顺序手术目标预计耗时风险等级
第一刀提取API请求到独立的service文件2小时⭐(低)
第二刀提取权限判断逻辑到自定义Hook3小时⭐⭐
第三刀拆分render函数,提取子组件4小时⭐⭐⭐
第四刀用策略模式重构那500行if/else1天⭐⭐⭐⭐
2. 开启“手术模式”

每台“手术”遵循相同的流程:

  • 术前准备:确保当前代码是可运行状态,commit一次(哪怕代码很烂,先commit)
  • 手术操作:只改动目标范围内的代码,其他代码碰都不要碰
  • 术中测试:改完一点,立即测试相关功能是否正常
  • 术后缝合:运行整个项目,确保没有引入新的bug
  • 提交记录:commit,并写下清晰的commit message

实战示例:提取API请求

// 重构前:组件里直接发请求
const UserManage = () => {
  const [users, setUsers] = useState([]);
  
  useEffect(() => {
    fetch('/api/users')
      .then(res => res.json())
      .then(data => setUsers(data));
  }, []);
  
  // 还有2000行其他代码...
}

// 重构后:第一步,只把请求移出去
// services/userService.js
export const userService = {
  getUsers: () => fetch('/api/users').then(res => res.json()),
  // 其他API方法...
}

// components/UserManage.jsx
import { userService } from '../services/userService';

const UserManage = () => {
  const [users, setUsers] = useState([]);
  
  useEffect(() => {
    userService.getUsers().then(setUsers);
  }, []);
  
  // 其他代码暂时不动
}
3. 使用“手术器械”

善用IDE的重构工具:

  • VS Code:选中代码 → 右键 → 重构提取(提取函数、提取常量、提取变量)
  • 快捷键Ctrl+Shift+R 调出重构菜单
4. 留下“手术记录”

每次重构完成后,在commit message里写清楚做了什么:

git commit -m "refactor: 提取用户管理模块的API请求到独立service
- 创建 userService.js,封装所有用户相关接口
- 移除组件内直接调用fetch的代码
- 为后续单元测试做准备
- 功能无变化,所有接口测试通过"

为什么要“一次只动一个点”?
因为可逆才能可控。如果一次改了100个文件,出问题了都不知道回滚到哪。但如果你只改了1个文件,出了问题,git checkout一下就回来了。


技巧三:“以终为始”重构 —— 让代码为未来而生

核心思想:重构不只是让代码变好看,而是让代码更容易应对未来的需求变化。站在未来看现在,你的代码需要具备什么样的能力?

具体做法

1. 识别“未来可能变”的点

根据业务经验,预判哪些地方未来最可能改动:

  • 业务规则:比如优惠计算方式、权限校验逻辑
  • UI表现:比如主题切换、组件样式
  • 数据来源:比如从REST API换成GraphQL
  • 第三方依赖:比如换一个图表库
2. 应用“面向未来”的设计模式

针对不同场景,选择合适的设计模式:

场景一:大量的if/else判断业务类型

// 重构前:每次加新类型都要改这里
const getPrice = (type, basePrice) => {
  if (type === 'student') {
    return basePrice * 0.8;
  } else if (type === 'member') {
    return basePrice * 0.9;
  } else if (type === 'vip') {
    return basePrice * 0.7;
  } else if (type === 'seasonal') {
    return basePrice * 0.5; // 还要判断季节...
  }
  // 未来还会有更多类型
}

// 重构后:策略模式,开闭原则
const priceStrategies = {
  student: (price) => price * 0.8,
  member: (price) => price * 0.9,
  vip: (price) => price * 0.7,
  // 未来加新类型,只需要在这里加一个策略
}

const getPrice = (type, basePrice) => {
  const strategy = priceStrategies[type] || ((price) => price);
  return strategy(basePrice);
}

场景二:组件里混杂各种UI状态

// 重构前:一个组件处理loading、error、empty、normal
const UserList = () => {
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [data, setData] = useState([]);
  
  if (loading) return <Spinner />;
  if (error) return <Error message={error} />;
  if (data.length === 0) return <Empty />;
  return <List data={data} />;
}

// 重构后:状态驱动,未来加新状态(比如网络重连)很容易
const USER_LIST_STATES = {
  LOADING: 'loading',
  ERROR: 'error',
  EMPTY: 'empty',
  SUCCESS: 'success'
}

const UserList = () => {
  const [state, setState] = useState(USER_LIST_STATES.LOADING);
  const [data, setData] = useState([]);
  
  const stateComponents = {
    [USER_LIST_STATES.LOADING]: <Spinner />,
    [USER_LIST_STATES.ERROR]: <Error onRetry={fetchData} />,
    [USER_LIST_STATES.EMPTY]: <Empty />,
    [USER_LIST_STATES.SUCCESS]: <List data={data} />
  }
  
  return stateComponents[state] || null;
}
3. 建立“代码防腐层”

对于那些你不能改(比如第三方SDK、后端接口)但可能会变的依赖,加一层防腐:

// 重构前:直接使用第三方库
// 如果哪天这个库不维护了,换库要改几百个文件
const result = moment(date).format('YYYY-MM-DD');

// 重构后:封装一层,隔离变化
// utils/dateUtil.js
import moment from 'moment';

export const formatDate = (date, format = 'YYYY-MM-DD') => {
  return moment(date).format(format);
}

// 如果以后要换成day.js,只需要改这一个文件
4. 编写“未来可读”的代码
  • 命名即注释:不要用datarestemp,用userListapiResponseformattedPrice
  • 复杂度治理:一个函数超过50行?拆!一个组件超过300行?拆!
  • 依赖显式化:不要用隐式的全局状态,用props、context、hooks显式传递

把这三大技巧写成文章的配图建议

为了让文章更生动,建议配这几张图:

  1. 技巧一的配图:一张手绘风格的思维导图,展示那个2000行组件被拆解后的逻辑分支,旁边放一个拿着放大镜的小人(考古学家)
  2. 技巧二的配图:一张手术室的照片风格图,把代码文件画成躺在手术台上的病人,旁边放一排“手术器械”(函数提取、变量提取、组件拆分等工具)
  3. 技巧三的配图:一张时间轴,左边是过去(屎山代码),中间是现在(重构中),右边是未来(新需求轻松接入),画几条线从“未来”指向代码的不同模块,表示这些地方预留了扩展点
  4. 封面图建议:用Canva做一张图,标题《3个技巧,让老代码起死回生》,背景放一段模糊的乱码代码,前景放一个干净整洁的代码片段,中间画一个箭头和“重构”二字

把这三大技巧写成文章的段落标题建议

技巧段落小标题建议
技巧一“看不懂的代码不要改”:我是怎么给屎山画CT片的
技巧二“一次只动一个点”:外科手术式的精准重构
技巧三“为未来而重构”:让代码长出应对变化的能力

这三个技巧层层递进:先看懂(考古)→ 再动手(手术)→ 最后升华(面向未来)