写在前面
很多前端开发者对"模块化"的理解,长期停留在"文件拆分"的物理层面。
比如:一个 Vue/React 组件写了 1000 行,觉得太乱了,于是把里面的三个函数提取出来,扔到 utils.js 里;把 HTML 里的弹窗拆出来,扔到 components/Modal.vue 里。做完这些,看着只有 200 行的主文件,心里一阵舒爽:"啊,我做好了模块化。"
这是错觉。
如果你只是把一团乱麻的代码切成了五段乱麻,那这不叫模块化,这叫 "分布式屎山" 。
在架构师的眼里,模块化不是为了把文件变小,而是为了治理复杂性 。它是关于边界(Boundaries) 、内聚(Cohesion) 和 耦合(Coupling) 的艺术。本篇我们将抛开具体的语法,探讨如何建立架构级别的模块化思维。

一、 什么是模块?从"物理文件"到"逻辑单元"
初级工程师看模块,看到的是文件后缀(.js, .vue, .tsx);架构师看模块,看到的是职责的边界 。
1.1 模块化的三个层级
我们的认知通常经历了三个阶段的进化:
- 语法级模块化 (Syntax Level): 这是最基础的。AMD、CommonJS、ES Modules。解决的是命名空间污染 和脚本加载顺序的问题。这是 2015 年前我们要解决的主要矛盾,现在已经变成了像空气一样的基础设施。
- 文件级模块化 (File Level): 为了代码复用,我们将通用逻辑提取为
hooks,将 UI 提取为components。这是目前绝大多数中级工程师所处的阶段。但如果不小心,很容易陷入 "为了拆分而拆分" 的陷阱。 - 领域级模块化 (Domain Level): 这是架构师关注的层面。一个模块不再是一个文件,而是一个业务能力的集合 。 比如"用户模块",它可能包含
UserCard.tsx(UI)、useUser.ts(逻辑)、user-service.ts(API)、UserType.ts(类型)。它们在物理上可能分散,但在逻辑上是一个整体。只有当这个整体对外暴露极其有限的接口,而隐藏内部所有复杂度时,它才是一个真正的模块。
1.2 架构师的视角:隐藏而非暴露
软件工程大师 David Parnas 早在 1972 年就提出了一个振聋发聩的观点:
"模块化的核心在于你隐藏了什么,而不是你暴露了什么。"
在前端开发中,我们经常犯的错误是暴露过多。
- 错误示范: 一个
DatePicker组件,通过props把内部的calendarInstance暴露给父组件,允许父组件直接操作日历内部状态。 - 架构灾难: 这意味着父组件和子组件形成了隐性耦合。一旦哪天你要把底层的日历库从 Moment.js 换成 Day.js,整个应用可能都会崩溃。
真正的模块化思维是"黑盒思维": 外部只管输入(Props/Params)和输出(Events/Return Values),绝不关心内部是如何实现的。
二、 核心心法:高内聚与低耦合的辩证关系
这八个字被说烂了,但真正能做到的寥寥无几。在前端语境下,它们有具体的落地含义。
2.1 什么是"真内聚"?(True Cohesion)
很多项目习惯按"技术类型"分目录:
src/
├── components/ (放所有组件)
├── hooks/ (放所有钩子)
├── utils/ (放所有工具)
├── types/ (放所有类型)
这看起来很整洁,其实是 "假内聚" (或者叫偶然内聚)。 当你需要修改"登录"功能时,你需要去 components 改表单,去 hooks 改逻辑,去 types 改接口定义。你的修改行为是跨越空间 的。
现代前端架构推崇的"真内聚"是按"功能特性(Feature)"组织:
src/
├── features/
│ ├── auth/ (登录模块:包含自己的 components, hooks, types)
│ ├── dashboard/ (大盘模块)
判定标准: 那些只有在一起工作才有意义 的代码,必须物理上就在一起。共同封闭原则(CCP) 告诉我们:将那些会因为相同理由而修改的类/文件,聚合在一起。
2.2 什么是"低耦合"?(Loose Coupling)
耦合不可避免,没有耦合的代码就是一堆死代码。架构师要做的是治理耦合的类型。
- 内容耦合(最差): 直接修改另一个模块的内部数据。比如组件 A 通过
ref强行修改组件 B 的 state。 - 控制耦合(较差): 传递 flag 告诉另一个模块该怎么做。比如
Button组件接收一个isLoginButton的 prop,导致 Button 内部包含了业务逻辑。 - 数据耦合(推荐): 仅仅传递数据。组件只接收它需要渲染的数据,不关心数据来源。
- 事件/消息耦合(最优): 通过发布订阅或回调函数通信。我不直接调用你,我只广播"我做完了",谁关心谁就来处理。
架构师的刀法: 当你发现两个模块必须同时修改才能跑通时,它们就是强耦合的。要么把它们合并成一个模块,要么引入一个中间层(适配器)来解耦。
三、 边界思维:如何切分模块?
在拿到一个复杂的业务需求(比如一个在线协作文档编辑器)时,普通开发者的第一反应是画页面,而架构师的第一反应是划边界 。
3.1 不稳定的依赖要隔离
稳定依赖原则(SDP): 依赖关系应该指向更稳定的方向。
- UI 是不稳定的:产品经理今天要把按钮放左边,明天要放右边,后天要换个颜色。
- 业务逻辑是相对稳定的:文档的保存、协同算法、权限校验,这些核心逻辑不会轻易变。
- 基础库是最稳定的:React 框架、Lodash 工具函数。
模块化切分策略: 绝不要把核心业务逻辑写在 UI 组件里(Vue 的 script 或 React 的 useEffect)。 Headless(无头化) 是前端架构的必然趋势。你应该把逻辑抽离成纯 JS/TS 模块(Hook 或 Class),UI 只是一个只有 render 函数的笨蛋壳子。这样,当 UI 翻天覆地变化时,你的核心逻辑模块可以纹丝不动。
3.2 循环依赖是架构的癌细胞
如果 A 模块引用了 B,B 又引用了 A,这在文件层面可能通过 Webpack 解决了,但在逻辑层面,这意味着 A 和 B 锁死在了一起,无法单独测试,无法单独复用。
如何打破循环?
- 下沉法: 找到 A 和 B 共同依赖的部分,抽取成 C 模块,A 和 B 都依赖 C。
- 反转法(依赖倒置 DIP): A 不直接依赖 B,A 定义一个接口(Interface),B 去实现这个接口。A 只依赖接口。
四、 模块化的代价:过度设计的陷阱
最后,必须给架构师们泼一盆冷水。模块化是有成本的。
模块化 = 增加间接层。
如果你把一个简单的"Hello World"拆成了 Provider、Service、Component、Type 四个文件,那你不是在做架构,你是在制造噪音 。
架构师的判断力体现在:
- 识别变化点: 只有那些未来极有可能发生变化 ,或者复杂度极高的地方,才值得被封装成独立模块。
- 适度冗余(DRY vs WET): 有时候,复制粘贴代码比错误的抽象更好。如果你强行把两个看似相似但业务背景完全不同的逻辑合并成一个模块,未来当它们向不同方向演进时,你将陷入无尽的
if (isModeA) else (isModeB)的地狱。
结语:从"写代码"到"设计系统"
模块化不是一种技术,而是一种世界观 。
当你开始思考**"如果我删掉这行代码,影响的范围是多大" ,或者 "如果我把这个文件夹移走,其他部分还能不能跑"**的时候,你就已经跨越了"文件拆分"的边界,开始用架构师的眼光审视你的系统了。
这只是思想的开篇。有了这个思维基石,接下来我们将深入骨架,探讨如何在具体的 UI 层面实现极致的逻辑与视图分离 。
Next Step: 思想已经建立,下一节我们将进入实战深水区。如何设计一个既能复用,又能灵活定制 UI 的组件? 请看**《第二篇:骨架(上)------组件化深度设计:逻辑与视图的极致分离(Headless UI)》**。