【前端重构】接手了 2 万行“屎山”Vue2 项目:我是如何在不重构的情况下稳住线上Bug的
📌 适用场景:接手遗留老项目、Vue2 迁移 Vue3 过渡期、维护无文档的旧代码
🛠 环境:Vue 2.6 / Webpack 4 / Element-UI / 项目体量 2万+ SLOC
💥 痛点:
this.$refs满天飞、全局变量污染、一次修改引发 10 个页面报错。
一、背景:那次让我通宵的“蝴蝶效应”
上周接手了一个 3 年前的后台管理系统。原开发者已离职,文档缺失。最大的问题是:一个看似简单的“导出Excel”功能,竟然耦合了 5 个组件的逻辑。
事故现场:
我只改了一行表单校验逻辑:
// 原代码
if (val === '') { ... }
// 我的修改
if (val == null || val.trim() === '') { ... }
结果导致完全不相关的“用户列表页”数据无法加载。报错日志如下:
TypeError: Cannot read properties of undefined (reading 'getList')
at VueComponent.submitForm (UserManage.vue?t=123456:189)
at eval (FormItem.vue?t=123456:67)
排查了 3 个小时,才发现是因为 UserManage.vue里直接通过 $parent.$parent调用了祖父组件的方法,而我修改的校验逻辑影响了父组件的 render时机。
这就是典型的“面条代码”:逻辑像意大利面一样搅在一起。
二、困境:为什么不能直接重构?
Leader 给我的要求是:一周内修复 5 个线上 Bug,但不能进行大规模重构(风险太高)。
面对这样的“屎山”,我制定了三个核心策略,不碰核心逻辑,只做“微创手术”。
三、策略 1:用“防腐层”隔离烂代码(Adapter 模式)
很多老代码直接调用 API,且参数格式混乱。我在 API 和页面之间加了一层 Adapter(适配器)。
重构前(页面直接调用,难以维护):
// UserPage.vue
methods: {
fetchData() {
// 参数格式完全依赖后端,且到处都是
api.getUserList({ page: this.page, pageSize: 10, type: 'admin' }).then(res => {
this.list = res.data.list;
});
}
}
重构后(加一层防腐层):
// services/userAdapter.js
import api from '@/api';
export function getUserListAdapter(params) {
// 在这里统一处理脏数据、参数映射
const query = {
pageNum: params.page ?? 1, // 处理空值
limit: params.pageSize || 10,
userType: params.type === 'admin' ? 1 : 0 // 转换格式
};
return api.getUserList(query);
}
// UserPage.vue
import { getUserListAdapter } from '@/services/userAdapter';
methods: {
async fetchData() {
try {
const res = await getUserListAdapter(this.params);
this.list = res.data.list;
} catch (err) {
console.error('数据加载失败,但页面不会崩:', err);
}
}
}
收益:API 变动只需要改 Adapter,页面代码纹丝不动。
四、策略 2:用“事件总线”解耦 $parent 依赖
为了解决 $parent.$parent这种脆弱的调用,我引入了极简的事件总线(Event Bus)来替代直接的组件依赖。
重构前(强耦合,改一处崩一片):
// 子组件
this.$parent.$parent.handleSearch(); // 极度依赖 DOM 结构
重构后(松耦合):
// event-bus.js
import Vue from 'vue';
export default new Vue();
// 子组件
import bus from '@/utils/event-bus';
bus.$emit('trigger-search', { keyword: 'test' }); // 只负责喊话
// 父组件 (created/mounted)
import bus from '@/utils/event-bus';
bus.$on('trigger-search', (params) => {
this.handleSearch(params); // 自己决定怎么处理
});
收益:组件不再关心谁调用了自己,只关心“发生了什么事”。
五、策略 3:用“防御性编程”兜底烂数据
老项目最大的问题是后端返回的数据结构不可信。我在所有关键渲染位置加了“安全漏斗”。
重构前(直接渲染,容易白屏):
<!-- 一旦 res.data.user 为空,页面直接报错 -->
<div>{{ res.data.user.info.name }}</div>
重构后(安全渲染):
<!-- 使用 v-if 或可选链操作符兜底 -->
<div v-if="res?.data?.user?.info">{{ res.data.user.info.name }}</div>
<!-- 或者在计算属性中处理 -->
computed: {
userName() {
// 防御性取值
return this.res?.data?.user?.info?.name || '未知用户';
}
}
收益:极大提升了页面的健壮性,不会因为某个字段缺失而导致整个模块崩溃。
六、总结与数据对比
经过一周的“微创”治理,效果显著:
|
指标 |
治理前 |
治理后 |
变化 |
|---|---|---|---|
|
线上 Bug 数 |
5个/周 |
1个/周 |
⬇️ 80% |
|
代码耦合度 |
高(直接依赖) |
中(事件驱动) |
✅ 改善 |
|
新功能开发 |
不敢动 |
可局部修改 |
✅ 提效 |
|
重构风险 |
极高 |
可控 |
✅ 安全 |
核心感悟:
面对遗留系统,不要想着一口吃成胖子。“止住出血”比“切除肿瘤”更重要。 通过 Adapter 隔离变化、通过 Event Bus 解耦通信、通过防御性编程提高容错,是治理“屎山”代码的三板斧。
⚠️ 适用边界:
本方案适用于维护期、风险敏感型的老项目。如果你的项目处于开发初期,请务必使用 Vue3 + Composition API + TypeScript,从源头上避免“屎山”的产生。
💬 互动话题:
你接手过最离谱的代码是什么样的?有没有见过 if (a == b || a == b)这种神来之笔?或者为了修复一个 Bug 不得不写下更烂的 Patch?欢迎在评论区分享你的“除屎”经历,让我们一起苦中作乐!
更多推荐
所有评论(0)