1.1 那个让我失眠的周五下午
入职第一天,领导把一个"简单任务"丢给我:"这个后台系统你熟悉一下,下周加几个小功能。"
我打开项目,找到他说的那个"核心组件"。第一眼,我以为是压缩后的代码------一个文件,2000多行。第二眼,我确认了,这是人写的。第三眼,我想起了那个著名的笑话:这段代码,只有上帝和它的原作者看得懂,而现在,原作者跳槽了,上帝说他也看不懂。
那一刻,我闻到了屎山的味道。
1.2 "精彩片段"
typescript
// 片段1:变量命名艺术
const data = props.data || [];
const data2 = localStorage.getItem('data2');
const data3 = this.props.data3; // 为什么从三个地方拿数据?不知道
const d = data.filter(...); // d是什么?大概是过滤后的数据吧
// 片段2:函数长度之最
const handleEverything = (type, value, callback, flag, ...args) => {
// 这个函数有300行
// 它处理表单提交、按钮点击、路由跳转、数据格式化...
// 甚至还有一个setTimeout里面套着另一个setTimeout
if (type === 'submit') { /* 100行 */ }
if (type === 'click') { /* 80行 */ }
if (flag === true) { /* 还有逻辑 */ }
// 300行后...
callback && callback();
}
// 片段3:神秘的魔法数字
<div style={{ marginTop: 17, paddingLeft: 13 }}> {/* 为什么是17和13?设计师的生日?*/}
{status === 2 && <span>进行中</span>} {/* 2代表什么?后面还有3、4、5... */}
{status === 4 && <span>已完成</span>}
</div>
2.1 第一步:源码考古
我没有急着改代码。我知道,对于一个陌生的系统,直接动手等于自杀。我打开Xmind,开始给这段代码画"CT片"。
scss
组件 (UserManage)
├── 初始化 (useEffect)
│ ├── 获取用户列表 (/api/users) → 存入state.users
│ ├── 获取部门列表 (/api/depts) → 存入state.depts
│ ├── 获取角色列表 (/api/roles) → 存入state.roles
│ └── 检查权限 (checkPermission) → 存入state.canEdit
├── 状态管理 (useState x 15个)
│ ├── users, depts, roles, permissions...
│ ├── loading, error, submitting, editing...
│ ├── modalVisible, modalType, modalData...
│ └── searchKeyword, searchResult, searchHistory...
├── 事件处理 (handleXxx x 20个)
│ ├── handleSearch, handleAdd, handleEdit, handleDelete
│ ├── handleModalOk, handleModalCancel, handleModalChange
│ ├── handleSelectRow, handleSelectAll, handleExport
│ └── handlePageChange, handlePageSizeChange
├── 渲染逻辑 (renderXxx x 8个)
│ ├── renderSearchBar, renderTable, renderPagination
│ ├── renderModal, renderDrawer, renderToolbar
│ ├── renderLoading, renderEmpty, renderError
│ └── renderActionButtons (里面还有权限判断)
└── 副作用 (useEffect x 6个)
├── 监听search变化 → 重新搜索
├── 监听page变化 → 重新加载
├── 监听selectedRows → 更新按钮状态
└── 监听权限变化 → 隐藏/显示按钮
2.2 第二步:代码体检 ------ 给问题分类
根据逻辑整理和各种分类后,我整理出了这个组件的"体检报告":
| 问题类型 | 具体表现 | 严重程度 |
|---|---|---|
| 单一职责 | 一个组件同时负责:数据获取、状态管理、UI渲染、权限控制、路由跳转 | ⭐⭐⭐⭐⭐ |
| 代码重复 | 相同的API调用逻辑出现在3个地方;相同的表单校验写了5遍 | ⭐⭐⭐⭐ |
| 魔法数字/字符串 | status用1/2/3/4表示,type用'A'/'B'/'C'表示 | ⭐⭐⭐⭐ |
| 命名混乱 | data, data1, data2, temp, temp2, res, result 满天飞 | ⭐⭐⭐ |
| 副作用管理 | 6个useEffect互相触发,形成死循环,只能用setTimeout打断 | ⭐⭐⭐⭐⭐ |
| 类型安全 | 没有TypeScript,props校验时有时无,经常传错参数 | ⭐⭐⭐⭐ |
2.3 第三步:制定重构计划
有了体检报告,我制定了分阶段的重构计划。这不是一蹴而就的事情,我给自己定了一个月的"慢性治疗"时间。

第三部分:重构实战
3.1 技巧一:先拆后合 ------ 用"组件拆分"解决2000行的问题
面对2000行的庞然大物,我的第一个想法是:能不能把它拆成多个小文件?就像切蛋糕一样,找到自然的切分点。
步骤1:识别UI区块
观察render函数,找到可以独立出来的UI模块:
javascript
// 重构前的render(简化版)
render() {
return (
<div className="user-manage">
{/* 1. 搜索栏 */}
<div className="search-bar">
<input value={keyword} onChange={onSearch} />
<button onClick={handleSearch}>搜索</button>
</div>
{/* 2. 操作工具栏 */}
<div className="toolbar">
<button onClick={handleAdd}>新增</button>
<button onClick={handleExport}>导出</button>
{canDelete && <button onClick={handleDelete}>删除</button>}
</div>
{/* 3. 表格 */}
<table>
{/* 200行的表格渲染逻辑 */}
{data.map(item => (
<tr key={item.id}>
<td>{item.name}</td>
{/* 还有很多列... */}
</tr>
))}
</table>
{/* 4. 分页 */}
<Pagination
current={page}
total={total}
onChange={handlePageChange}
/>
{/* 5. 新增/编辑弹窗 */}
{modalVisible && (
<Modal title={modalType === 'add' ? '新增' : '编辑'}>
{/* 100行的表单 */}
</Modal>
)}
</div>
)
}
步骤2:逐个提取子组件
创建第一个子组件:SearchBar.jsx
ini
// components/SearchBar.jsx
const SearchBar = ({ keyword, onSearch, onKeywordChange }) => {
return (
<div className="search-bar">
<Input
value={keyword}
onChange={onKeywordChange}
placeholder="输入用户名/手机号搜索"
allowClear
/>
<Button type="primary" onClick={onSearch}>
搜索
</Button>
</div>
);
};
// 加上PropTypes校验
SearchBar.propTypes = {
keyword: PropTypes.string,
onSearch: PropTypes.func.isRequired,
onKeywordChange: PropTypes.func.isRequired
};
export default SearchBar;
步骤3:递归拆分
按照同样的方式,继续提取:
UserTable.jsx:表格组件Toolbar.jsx:操作栏组件UserModal.jsx:弹窗组件PaginationBar.jsx:分页组件
重构后的主组件:
ini
// UserManage.jsx 重构后
import SearchBar from './SearchBar';
import Toolbar from './Toolbar';
import UserTable from './UserTable';
import PaginationBar from './PaginationBar';
import UserModal from './UserModal';
const UserManage = () => {
// 状态管理
const [keyword, setKeyword] = useState('');
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
const [pagination, setPagination] = useState({ page: 1, pageSize: 10, total: 0 });
const [modal, setModal] = useState({ visible: false, type: 'add', data: null });
// 业务逻辑
const handleSearch = () => { /* 搜索逻辑 */ };
const handleAdd = () => { /* 新增逻辑 */ };
const handleEdit = (user) => { /* 编辑逻辑 */ };
const handleDelete = (ids) => { /* 删除逻辑 */ };
return (
<div className="user-manage">
<SearchBar
keyword={keyword}
onKeywordChange={setKeyword}
onSearch={handleSearch}
/>
<Toolbar
onAdd={handleAdd}
onExport={handleExport}
canDelete={hasPermission('delete')}
selectedRows={selectedUsers}
/>
<UserTable
data={users}
loading={loading}
onEdit={handleEdit}
onDelete={handleDelete}
onSelect={handleSelect}
/>
<PaginationBar
{...pagination}
onChange={handlePageChange}
/>
<UserModal
visible={modal.visible}
type={modal.type}
data={modal.data}
onOk={handleModalOk}
onCancel={handleModalCancel}
/>
</div>
);
};
效果对比:
- 重构前:1个文件,2000行
- 重构后:6个文件,每个200-400行
- 可维护性:⭐⭐⭐⭐⭐(可以独立修改、测试每个组件)
3.2 技巧二:逻辑抽离 ------ 用自定义Hooks消灭"面条式代码"
拆完UI后发现,主组件虽然变短了,但里面的逻辑仍然很混乱。各种useState、useEffect、事件处理函数挤在一起,像一个没有整理的工具箱。
步骤1:识别逻辑分组
观察现有的逻辑,可以分为几类:
- 用户数据逻辑:获取用户列表、搜索、分页
- 弹窗逻辑:打开/关闭、编辑/新增模式
- 权限逻辑:判断是否有操作权限
- 选择逻辑:表格行选择、全选/取消
步骤2:创建第一个自定义Hook ------ useUsers
ini
// hooks/useUsers.js
import { useState, useEffect } from 'react';
import { userService } from '../services/userService';
export const useUsers = (initialPagination) => {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
const [pagination, setPagination] = useState(initialPagination);
const [filters, setFilters] = useState({ keyword: '' });
// 加载用户数据
const fetchUsers = async () => {
setLoading(true);
try {
const { page, pageSize } = pagination;
const { keyword } = filters;
const result = await userService.getUsers({
page,
pageSize,
keyword
});
setUsers(result.list);
setPagination(prev => ({
...prev,
total: result.total
}));
} catch (error) {
console.error('获取用户列表失败:', error);
} finally {
setLoading(false);
}
};
// 当分页或搜索条件变化时,重新加载
useEffect(() => {
fetchUsers();
}, [pagination.page, pagination.pageSize, filters.keyword]);
// 搜索
const search = (keyword) => {
setFilters({ keyword });
setPagination(prev => ({ ...prev, page: 1 })); // 重置到第一页
};
// 改变页码
const changePage = (page, pageSize) => {
setPagination(prev => ({
...prev,
page,
pageSize: pageSize || prev.pageSize
}));
};
return {
users,
loading,
pagination,
search,
changePage,
refresh: fetchUsers
};
};
步骤3:创建更多自定义Hook
ini
// hooks/useModal.js
export const useModal = () => {
const [visible, setVisible] = useState(false);
const [type, setType] = useState('add'); // 'add' | 'edit' | 'view'
const [data, setData] = useState(null);
const openAdd = () => {
setType('add');
setData(null);
setVisible(true);
};
const openEdit = (record) => {
setType('edit');
setData(record);
setVisible(true);
};
const close = () => {
setVisible(false);
// 延迟清除数据,避免弹窗关闭动画期间数据消失
setTimeout(() => {
setData(null);
}, 300);
};
return {
modal: { visible, type, data },
openAdd,
openEdit,
close
};
};
// hooks/useSelection.js
export const useSelection = (dataSource, rowKey = 'id') => {
const [selectedKeys, setSelectedKeys] = useState([]);
const [selectedRows, setSelectedRows] = useState([]);
const select = (keys, rows) => {
setSelectedKeys(keys);
setSelectedRows(rows);
};
const clearSelection = () => {
setSelectedKeys([]);
setSelectedRows([]);
};
const isSelected = (record) => {
const key = record[rowKey];
return selectedKeys.includes(key);
};
return {
selectedKeys,
selectedRows,
select,
clearSelection,
isSelected
};
};
步骤4:重构主组件,使用这些Hooks
ini
// UserManage.jsx 最终版
import SearchBar from './SearchBar';
import Toolbar from './Toolbar';
import UserTable from './UserTable';
import PaginationBar from './PaginationBar';
import UserModal from './UserModal';
import { useUsers } from '../hooks/useUsers';
import { useModal } from '../hooks/useModal';
import { useSelection } from '../hooks/useSelection';
const UserManage = () => {
// 使用自定义Hooks
const {
users,
loading,
pagination,
search,
changePage,
refresh
} = useUsers({ page: 1, pageSize: 10, total: 0 });
const {
modal,
openAdd,
openEdit,
close
} = useModal();
const {
selectedKeys,
selectedRows,
select,
clearSelection
} = useSelection(users);
// 处理弹窗提交
const handleModalOk = async (values) => {
if (modal.type === 'add') {
await userService.addUser(values);
} else {
await userService.updateUser(modal.data.id, values);
}
close();
refresh(); // 刷新列表
clearSelection();
};
return (
<div className="user-manage">
<SearchBar
onSearch={search}
/>
<Toolbar
onAdd={openAdd}
onExport={handleExport}
canDelete={hasPermission('delete')}
selectedRows={selectedRows}
/>
<UserTable
data={users}
loading={loading}
onEdit={openEdit}
onDelete={handleDelete}
selectedKeys={selectedKeys}
onSelect={select}
/>
<PaginationBar
current={pagination.page}
pageSize={pagination.pageSize}
total={pagination.total}
onChange={changePage}
/>
<UserModal
visible={modal.visible}
type={modal.type}
data={modal.data}
onOk={handleModalOk}
onCancel={close}
/>
</div>
);
};
3.3 技巧三:类型安全 ------ 用TypeScript让代码"自文档化"
重构的最后一步,也是最关键的一步:引入TypeScript。这不仅是为了类型检查,更是为了让代码变成"自文档",让下一个接手的人不再迷茫。
步骤1:定义数据模型
ini
// types/user.ts
export interface User {
id: string;
username: string;
nickname: string;
email: string;
phone: string;
avatar?: string;
status: UserStatus;
role: UserRole;
department: Department;
createdAt: string;
updatedAt: string;
lastLoginAt?: string;
}
export enum UserStatus {
INACTIVE = 0, // 未激活
ACTIVE = 1, // 正常
LOCKED = 2, // 锁定
DELETED = -1 // 已删除
}
export enum UserRole {
VISITOR = 'visitor', // 访客
MEMBER = 'member', // 普通成员
MANAGER = 'manager', // 管理员
SUPER_ADMIN = 'super_admin' // 超级管理员
}
export interface Department {
id: string;
name: string;
parentId: string | null;
path: string;
}
export interface PaginationParams {
page: number;
pageSize: number;
keyword?: string;
status?: UserStatus;
departmentId?: string;
}
export interface PaginationResult<T> {
list: T[];
total: number;
page: number;
pageSize: number;
}
步骤2:给组件加上类型
typescript
// components/UserTable.tsx
import React from 'react';
import { Table, Tag, Button, Space, message } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { User, UserStatus, UserRole } from '../types/user';
interface UserTableProps {
data: User[];
loading: boolean;
onEdit: (user: User) => void;
onDelete: (userId: string) => void;
selectedKeys: string[];
onSelect: (keys: string[], rows: User[]) => void;
}
const UserTable: React.FC<UserTableProps> = ({
data,
loading,
onEdit,
onDelete,
selectedKeys,
onSelect
}) => {
// 状态标签的颜色映射
const statusColorMap: Record<UserStatus, string> = {
[UserStatus.ACTIVE]: 'green',
[UserStatus.INACTIVE]: 'orange',
[UserStatus.LOCKED]: 'red',
[UserStatus.DELETED]: 'gray'
};
// 状态文本的映射
const statusTextMap: Record<UserStatus, string> = {
[UserStatus.ACTIVE]: '正常',
[UserStatus.INACTIVE]: '未激活',
[UserStatus.LOCKED]: '已锁定',
[UserStatus.DELETED]: '已删除'
};
// 表格列定义
const columns: ColumnsType<User> = [
{
title: '用户名',
dataIndex: 'username',
key: 'username',
render: (text, record) => (
<Space>
{record.avatar && <Avatar src={record.avatar} size="small" />}
<span>{text}</span>
</Space>
)
},
{
title: '昵称',
dataIndex: 'nickname',
key: 'nickname'
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render: (status: UserStatus) => (
<Tag color={statusColorMap[status]}>
{statusTextMap[status]}
</Tag>
)
},
{
title: '角色',
dataIndex: 'role',
key: 'role',
render: (role: UserRole) => role.toUpperCase()
},
{
title: '部门',
dataIndex: ['department', 'name'],
key: 'department'
},
{
title: '最后登录',
dataIndex: 'lastLoginAt',
key: 'lastLoginAt',
render: (date: string) => date ? new Date(date).toLocaleString() : '-'
},
{
title: '操作',
key: 'action',
render: (_, record) => (
<Space>
<Button type="link" onClick={() => onEdit(record)}>
编辑
</Button>
<Button
type="link"
danger
onClick={() => onDelete(record.id)}
>
删除
</Button>
</Space>
)
}
];
return (
<Table
rowKey="id"
columns={columns}
dataSource={data}
loading={loading}
rowSelection={{
selectedRowKeys: selectedKeys,
onChange: onSelect
}}
pagination={false}
/>
);
};
export default UserTable;
步骤3:给自定义Hooks加上类型
ini
// hooks/useUsers.ts
import { useState, useEffect } from 'react';
import { userService } from '../services/userService';
import type { User, PaginationParams, PaginationResult } from '../types/user';
interface UseUsersOptions {
initialPage?: number;
initialPageSize?: number;
autoLoad?: boolean;
}
interface UseUsersResult {
users: User[];
loading: boolean;
pagination: {
page: number;
pageSize: number;
total: number;
};
search: (keyword: string) => void;
changePage: (page: number, pageSize?: number) => void;
refresh: () => Promise<void>;
}
export const useUsers = (options: UseUsersOptions = {}): UseUsersResult => {
const {
initialPage = 1,
initialPageSize = 10,
autoLoad = true
} = options;
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(false);
const [pagination, setPagination] = useState({
page: initialPage,
pageSize: initialPageSize,
total: 0
});
const [filters, setFilters] = useState<Partial<PaginationParams>>({});
const fetchUsers = async () => {
setLoading(true);
try {
const params: PaginationParams = {
page: pagination.page,
pageSize: pagination.pageSize,
...filters
};
const result: PaginationResult<User> = await userService.getUsers(params);
setUsers(result.list);
setPagination(prev => ({
...prev,
total: result.total
}));
} catch (error) {
console.error('获取用户列表失败:', error);
// 可以在这里处理错误,比如显示错误提示
} finally {
setLoading(false);
}
};
useEffect(() => {
if (autoLoad) {
fetchUsers();
}
}, [pagination.page, pagination.pageSize, filters]);
const search = (keyword: string) => {
setFilters({ keyword });
setPagination(prev => ({ ...prev, page: 1 }));
};
const changePage = (page: number, pageSize?: number) => {
setPagination(prev => ({
...prev,
page,
pageSize: pageSize || prev.pageSize
}));
};
return {
users,
loading,
pagination,
search,
changePage,
refresh: fetchUsers
};
};
第四部分:重构成果展示
4.1 量化指标对比
| 指标 | 重构前 | 重构后 | 改善 |
|---|---|---|---|
| 文件大小 | 2000行/1个文件 | 6个文件,平均300行 | 可维护性大幅提升 |
| 代码重复率 | 35% | 5% | -30% |
| 圈复杂度 | 25 | 8 | -68% |
| 测试覆盖率 | 0% | 85% | +85% |
| 新增功能耗时 | 3天 | 0.5天 | -83% |
4.2 新增功能演示
重构完成后的第二周,产品经理过来说:"我们要加一个功能:批量导出用户数据,按部门筛选。"
重构前的改动方式:
- 在2000行的组件里找哪里获取数据
- 加一个按钮,加一个事件处理
- 可能还要改分页逻辑
- 测试要花2天,因为怕改坏其他地方
重构后的改动方式:
ini
// 1. 在Toolbar组件里加一个按钮
<Toolbar
onAdd={openAdd}
onExport={handleExport} // 新增
onBatchExport={handleBatchExport} // 新增批量导出
/>
// 2. 在useUsers里加一个导出方法
const exportUsers = async (filters) => {
return await userService.exportUsers(filters);
};
// 3. 在UserManage里组合
const handleBatchExport = async () => {
await exportUsers({ departmentId: selectedDept });
message.success('导出成功,请到下载中心查看');
};
耗时:2小时完成,测试10分钟。
4.3 个人收获
这次重构让我深刻体会到:
- 好的代码是改出来的,不是写出来的。那个2000行的组件,可能最初也只有200行,只是随着需求迭代,慢慢长成了怪物。
- 重构不是重写。我保留了所有的业务逻辑,只是改变了组织形式。这让我在重构过程中,随时可以上线,不影响业务。
- 可读性 > 炫技。重构后的代码,用了很多"笨"方法------明确的命名、简单的逻辑、标准的模式。新人来了也能很快上手。
第五部分:结尾
5.1 给读者的建议
如果你也面临一个"祖传代码",不要慌,记住这三点:
- 先看懂,再动手:用思维导图画出代码的地图
- 小步快跑,持续集成:每次只改一点,随时可以回滚
- 面向未来编程:让代码能轻松应对下一个需求
5.2 资源分享
这次重构过程中,我用到的工具和模板都整理好了:
GitHub\] 重构后的完整代码仓库(地址)
思维导图\] 我画的CT图模板
5.3 互动话题
你遇到过最离谱的"祖传代码"是什么样的?欢迎在评论区分享你的"屎山历险记"!