📌 适用场景:接手遗留老项目、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?欢迎在评论区分享你的“除屎”经历,让我们一起苦中作乐!

更多推荐