组件化,是在破坏分层吗?

0 阅读6分钟

如果你做过 React 或 Vue 的代码评审,大概率见过这种争论:

"为什么要把 HTML、CSS、JS 写在一个文件里?这不是违反了关注点分离吗?"

这个问题困扰了很多前端开发者。今天来把它彻底讲清楚。

结论先行:组件化不是在破坏分层,而是把分层的边界从"技术文件"移到了"功能单元"。

一、经典分层:看起来很整齐

Web 开发有一个延续了十几年的传统——按技术类型分文件:

项目结构(经典模型)
├── styles/       ← 所有 CSS
├── scripts/      ← 所有 JavaScript
└── templates/    ← 所有 HTML

HTML 管结构,CSS 管样式,JS 管行为。三层各司其职,教科书般的 Separation of Concerns。

这个模型在"以文档为中心"的年代确实好用——内容独立于表现,设计师写 CSS,JS 专家写脚本,大家互不干扰。

但当 Web 从"文档"变成"应用",问题就来了。

二、一个下拉菜单,改三个文件

想象你要改一个下拉菜单的交互:

关注点文件位置内容
结构templates/dropdown.htmlDOM 结构
样式styles/dropdown.css外观
行为scripts/dropdown.js开关逻辑

一个功能,散布在三个文件、三个目录、三种上下文中。要理解下拉菜单怎么工作,你需要同时在脑中持有三者;要修改它,需要触碰三处。

Martin Fowler 在《重构》里给这种代码坏味道起了个名字——霰弹式修改(Shotgun Surgery) :改一个功能,像打散弹枪一样,弹片散落到代码库各处。

这就好比你住在一座"严格功能分区"的城市:买菜要去商业区,上班要去工业区,回家要去住宅区。看起来规划整齐,但人在三个区之间来回跑,通勤成本极高。

我们优化的是技术边界,但真正的复杂性在功能上。

三、把边界搬到功能上

React、Vue、Svelte 做了一件关键的事——共置(Co-location) 。一起变化的东西,放在一起:

项目结构(现代模型)
├── components/
│   ├── Dropdown/       ← 下拉菜单的一切
│   │   ├── Dropdown.tsx
│   │   ├── Dropdown.css
│   │   └── Dropdown.test.ts
│   ├── Modal/          ← 模态框的一切
│   └── UserProfile/    ← 用户资料的一切

设计师要改下拉菜单外观?打开 Dropdown 目录。PM 要求加键盘导航?还是 Dropdown。Bug 报告提到下拉菜单?依然是 Dropdown。

一个功能,一个位置。

这正是 Kent C. Dodds 总结的共置原则: "Things that change together should be located as close as reasonable."  一起变化的东西,应该尽可能地住在一起。

就像现代城市规划转向"混合用途社区"——一栋楼里同时有住宅、商铺、办公室。你不用横跨半座城去买瓶水,下楼就行。

按技术分离 vs 按功能分离

按技术分离 vs 按功能分离

四、组件内部,依然要分层

但共置不是说"把所有东西搅成一团"。组件的外部边界是功能,内部依然需要分层。

Den Odell 在 Frontend Patterns 里定义了五对真正的逻辑边界:

边界分什么为什么分
业务逻辑 vs 展示逻辑Smart 组件管数据,Dumb 组件管渲染展示层可复用、可独立测试
纯代码 vs 副作用计算/转换 vs API 调用/定时器/埋点副作用被隔离,代码更可预测
服务端状态 vs 客户端状态React Query 管缓存 vs useState 管 UI两者生命周期不同,混在一起会乱
全局状态 vs 局部状态用户信息/路由 vs 模态框开关/hover全局状态需要集中管理,局部的不必
基础设施 vs 领域虚拟滚动/分页 vs 产品列表展示基础设施跨领域复用,领域逻辑专注业务

用生物学来打个比方:一个细胞就是一个组件。它自带 DNA(业务逻辑)、核糖体(展示层)、线粒体(副作用引擎)。你不需要跑到别的细胞去拿零件——但细胞内部的这些结构,是清晰分层的。

组件内部的分层结构

组件内部的分层结构

五、怎么判断你分对了?

原文给了四个检验问题,非常实用:

#问题没分好的信号
1改一个功能,是否只碰一个位置?改一处要跟着改三五个文件
2能一句话说清每个模块做什么?描述一个模块需要连续用"并且"
3能独立测试每个部分?测一件事要 mock 一堆不相关的东西
4不同人能并行工作而不冲突?所有人的改动总是互相踩

这四个问题的本质是在测量两个指标:内聚性(一个模块内部的东西有多相关)和耦合度(模块之间有多少牵连)。

好的分离 = 高内聚 + 低耦合。  这不是前端的发明,这是软件工程 50 年来最重要的原则之一。

六、一个很像的管理学问题

如果你在公司待过,一定见过两种组织架构:

维度按职能分部按产品线分部
前端前端部、后端部、设计部搜索团队、支付团队、会员团队
优势技术栈统一、专业度高响应快、交付独立、全功能闭环
痛点跨部门协调成本高可能重复造轮子
类比经典 Web 按技术分文件现代前端按功能分组件

亚马逊的 Two-Pizza Team 就是"按产品线分部"的典型——每个小团队拥有一个完整的功能模块,从前端到后端到数据库,一个披萨盒大小的团队搞定一切。

前端架构的演进,和组织架构的演进,走的是同一条路:从"按专业分"到"按交付单元分"。

七、对你写代码意味着什么?

给你一个实操判断框架:

改三个文件才能完成一个功能 → 你在错误的轴上做了分离

一个组件超过 300 行 → 它可能积累了多个关注点,该拆

测试写不下去 → 边界没划好,副作用和纯逻辑混在一起

说不清一个模块干什么 → 它承担了太多职责

记住 Dijkstra 的那句话:关注点分离是软件设计中最重要的原则。但原则是永恒的,应用是随场景变化的

经典 Web 按技术分,因为那时 Web 是文档。现代前端按功能分,因为 Web 变成了应用。两者都是同一个底层原则的正确应用。

如果你只想带走一句话,我建议记这个:

分离的目的不是整齐,是让变更更容易。

参考原文:

• Den Odell — Logical Separation of Concerns

qrcode_for_gh_6a9e7f3719d6_344.jpg