前端向架构突围系列 - 框架设计(二):糟糕的代码有哪些特点?

1 阅读6分钟

前言 你有没有过这种经历:新接手了一个项目,产品经理让你把一个按钮往左移 5 像素。你心想:“这不有手就行?” 结果你改了 CSS,保存,刷新。 按钮是移过去了,但登录弹窗打不开了,控制台红了一片,甚至 CI/CD 流程都挂了。

这一刻,你面对的不是代码,而是一座摇摇欲坠的屎山。 在框架设计和组件库开发中,这种现象尤为致命。业务代码写烂了,坑的是一个页面;框架设计写烂了,坑的是整个团队。今天我们要聊的不是具体的变量命名或缩进,而是架构层面的“设计臭味”

image.png

什么是“设计臭味”?

“代码臭味”(Code Smell)这个词不是说代码真的有味儿(虽然有时候看代码确实想吐),而是指代码结构中某些特征暗示了深层次的设计问题

它就像煤气泄漏的味道,本身不一定会炸,但只要有一点火星(新的需求变更),整个系统就会原地升天。

作为前端架构师或核心开发者,如果你在 Code Review 时闻到了以下这 5 种味道,请务必警惕。


1. 僵化性 (Rigidity):牵一发而动全身

症状: 你想复用一个通用的 Header 组件,结果发现它里面硬编码了 useRouter() 的跳转逻辑,甚至还直接 importRedux/Pinia 的 store。 你想在另一个项目用它?没门。除非你把那边的路由和状态管理全套搬过来。

前端实战翻译: 这就是典型的高耦合。组件不再是一个独立的乐高积木,而是一块焊死在主板上的芯片。

反面教材 (React):

// 这是一个充满僵化味道的组件
const UserProfile = () => {
  // 致命伤1:直接依赖具体的全局状态
  const user = useSelector(state => state.user.info);
  // 致命伤2:直接依赖具体的路由实现
  const history = useHistory();

  const handleLogout = () => {
     // 业务逻辑耦合在UI里
     api.logout().then(() => history.push('/login'));
  }

  return <div>{user.name} <button onClick={handleLogout}>退出</button></div>;
};

指南:

  • 控制反转 (IoC) :组件只管展示,逻辑通过 Props 传进来。
  • Presentational vs Container:把“展示组件”和“容器组件”拆开。展示组件要像“傻瓜”一样,给什么吃什么,不要自己去冰箱(Store)里拿。

2. 脆弱性 (Fragility):改东崩西的蝴蝶效应

症状: 这比僵化性更搞心态。僵化性是你改不动,脆弱性是你改了,但崩在了你完全想不到的地方。 比如:你为了优化首页加载速度,调整了一个公共 utils 函数,结果结算页面的金额计算错了,多给了用户 100 块钱。

前端实战翻译: 通常源于隐式依赖全局变量污染或者CSS 样式穿透

反面教材 (CSS/Vue):

/* 这种写法在全局样式里简直是灾难 */
.title {
  font-size: 20px;
  color: red;
}

/* 或者在组件里滥用 !important */
.btn {
  background: blue !important; /* 你的同事想覆盖这个样式时,必须写得比你更恶心 */
}

指南:

  • CSS Modules / Scoped CSS / Tailwind:坚决消灭全局样式冲突。
  • 纯函数 (Pure Functions) :工具类函数坚决不能有副作用,输入相同,输出必须相同。
  • 依赖显式化:别在组件里偷偷摸摸读 window.xxx 或者 localStorage,把它们封装成 Hooks 或服务。

3. 顽固性 (Immobility):无法拆分的连体婴

症状: 你写了一个非常炫酷的 DataGrid 表格,支持排序、筛选、分页。隔壁组看到了说:“哇,这个好,我也要用。” 你自信满满地把代码发给他。 五分钟后他跑来说:“哥,我只要个表格UI,你为什么把 Axios 拦截器、ElementUI 的弹窗组件、甚至你们公司的埋点 SDK 都打包进来了?”

前端实战翻译: 这是内聚性低的表现。业务逻辑和基础设施、UI 逻辑混在一起,导致根本无法拆分复用。

反面教材:

JavaScript

// 一个原本想做通用组件的 hook,却混入了业务
function useTableData(url) {
  const [data, setData] = useState([]);

  useEffect(() => {
    // 错误:这里耦合了特定的 HTTP 库和业务上的 token 逻辑
    axios.get(url, { headers: { 'X-Auth': localStorage.getItem('token') } })
      .then(res => setData(res.data.list)); // 错误:硬编码了数据结构 res.data.list
  }, [url]);

  return data;
}

指南:

  • Headless UI:这是现在的设计趋势(如 React Table, TanStack Query)。只提供逻辑钩子,不提供 UI。
  • 依赖倒置:网络请求层应该作为参数传入,而不是在组件内部直接实例化。

4. 粘滞性 (Viscosity):做错误的事更容易

症状: 这是一个人性问题。 假设你的框架支持 TypeScript。

  • 正确的做法:定义 Interface,继承 Props,处理泛型,写 Mock 数据,跑单元测试。需要 10 分钟。
  • 错误的做法any 一把梭。需要 10 秒钟。

做正确的事做错误的事阻力大得多时,开发者就会倾向于破坏架构。这就是粘滞性。

前端实战翻译: 环境配置太复杂、类型定义太反人类、测试难写。

反面教材: 如果你的组件库要求使用者必须写 5 层嵌套的配置对象才能跑起来,那使用者一定会想办法绕过配置,直接去改源码。

指南:

  • 约定优于配置:像 Next.js 或 Nuxt.js 那样,文件放对位置路由就自动生成了。
  • 提供开箱即用的类型:别让用户自己去猜泛型填什么。
  • 路径依赖设计:让最简单的写法,就是最佳实践。

5. 晦涩性 (Opacity) & 过度设计 (Needless Complexity)

症状: 这两个往往相伴而生。 你打开一个同事的代码,看到了一堆 AbstractFactoryProviderHighOrderComponentWrapper。 你只是想渲染一个输入框,结果你需要先创建一个 FormConfig,再实例化一个 FieldBuilder,最后通过 RenderProp 传进去。

开发者看着你的代码会感叹:“虽然看不懂,但感觉很厉害的样子。” —— 别傻了,他们心里在骂娘。

前端实战翻译: 为了封装而封装。比如把简单的 if-else 逻辑抽象成极其复杂的策略模式,或者写了无比抽象的 Hooks,结果参数传了 8 个,返回值 12 个。

指南:

  • YAGNI 原则 (You Ain't Gonna Need It):不要为你臆想的未来需求写代码。
  • 代码如文章:好的代码应该像大白话一样。如果一段代码需要你写 10 行注释来解释“我为什么要绕这么大弯子”,那通常意味着设计失败。
  • 组合优于继承,简单优于抽象:在前端,特别是 React Hooks 中,平铺直叙的逻辑往往比层层嵌套的高阶组件要好维护得多。

总结:如何避免成为“制造臭味”的人?

设计框架就像盖楼。

  • 僵化性是钢筋没绑好,想改户型得拆承重墙。
  • 脆弱性是地基没打牢,楼上装修楼下漏水。
  • 顽固性是水电管线混在一起,修电线得砸水管。
  • 粘滞性是垃圾道设计不合理,大家只好往楼下扔垃圾。

要去除这些“味道”,最核心的心法只有一句话:

保持代码的“软”度 (Software)。

软件之所以叫软件,是因为它应该是易于改变的。当我们写下一行代码时,多问自己一句: “如果明天这个需求变了,我今天写的这行代码是资产,还是债务?”


互动话题

你的项目里有没有那种“甚至不敢看它一眼,怕看一眼它就崩了”的代码?或者你见过最离谱的“过度设计”是什么样的?欢迎在评论区晒出你的“受苦”经历,让大家开心一下(划掉)避避坑。