当 React 项目从“单人几天就能搞定”的规模,发展到多人协作、长期迭代的大型应用时,代码组织方式直接决定了项目的可维护性与迭代效率。模块分层与业务域拆分是两项核心架构决策,目的都是让代码结构清晰、职责明确、变更影响可控。
为什么需要拆分
没有规划的代码往往会形成“一团乱麻”:
- 所有组件堆在
components/下,数百个文件难以查找。 - 业务逻辑、API 请求、UI 状态混在一起,改一个功能要翻遍整个项目。
- 多人并行开发时频繁出现文件冲突,合并代码成为噩梦。
分层的目标是纵向隔离关注点(UI 层不该直接操作数据库),业务域拆分的目标是横向切分业务边界(用户模块的变动不影响订单模块)。两者结合,形成网格状的模块架构。
模块分层:纵向隔离关注点
在 React 项目中,通常将代码按照技术职责划分为若干层,上层依赖下层,下层对上层无感。一个经典且实用的分层方案如下:
src/
├── infrastructure/ # 基础设施层:与框架无关的通用工具
├── services/ # 数据服务层:封装外部交互(API、本地存储)
├── stores/ # 状态管理层:全局状态、业务状态
├── hooks/ # 共享逻辑层:可复用的 Hooks
├── components/ # 通用 UI 组件层:无业务属性的展示组件
├── features/ # 业务特征层:聚合 UI + 业务逻辑的完整功能模块
└── pages/ # 页面组装层:路由对应的顶层页面,组合 features
各层职责:
- infrastructure:HTTP 客户端封装、通用工具函数(日期格式化、权限检查)、常量定义。与业务无关,可在多个项目间复用。
- services:定义如何获取数据,例如
userService.getProfile()、orderService.listOrders()。内部使用 infrastructure 提供的 http 客户端,返回的数据通常是“纯数据对象”,不含中间件逻辑。 - stores:如果用全局状态库,将 Store 定义放在这里。每个 Store 对应一个业务实体的状态(如
useUserStore、useCartStore)。注意 Store 应该只存储状态和简单的更新逻辑,复杂的协调逻辑放在 hooks 中。 - hooks:跨组件的业务逻辑封装,如
useAuth(登录/登出/权限判断)、useCart(加入购物车、计算总价)。hooks 通常依赖 services 获取数据,并调用 stores 更新状态,纯 UI 层的组件只负责触发 hooks 暴露的方法。 - components:纯粹的展示组件,不包含任何业务逻辑。例如
Button、Modal、DataTable、FormInput。它们是构成所有业务页面的原子积木,可以进一步抽取为独立的组件库。 - features:业务功能模块,组织方式是将一个完整功能需要的 UI 组件、专属 hooks、样式文件集中放在一起。每个 feature 目录内部可以有自己的 components、hooks、services 子分层。这是业务域拆分的载体。
- pages:路由对应的页面入口,组合多个 features,通常只负责布局和传递路由参数,不写复杂的业务逻辑。
示例:用户个人信息模块的调用链路
Page(ProfilePage)
└─ 使用 Feature(UserProfileFeature)
├─ 调用 Hook(useUserProfile) → 内部调用 userService.getProfile() + useUserStore
├─ 渲染 通用组件(Avatar, Button)
└─ 可能包含自己的子组件 UserDetails、EditForm
这样的分层让每一层都有明确的输入输出契约,测试和重构时可以分而治之。
业务域拆分:横向切分边界
仅仅有分层还不够,当一个项目包含用户、订单、商品、营销、数据报表等多个业务域时,所有代码仍可能因为业务边界模糊而耦合在一起。业务域拆分的基本原则是:按业务概念将代码组织成独立的功能模块,每个模块拥有自己的分层结构。
通常有两种主流拆分策略:
- 按业务域(Domain-driven)——适合多业务线并行开发,如电商/CMS/金融后台。
- 按页面/路由——适合业务边界不明显,或页面间功能独立的小型项目。
在大型应用中,推荐第一种方案,代码目录形如:
src/
├── domains/ # 或叫 modules/、features/
│ ├── user/ # 用户业务域
│ │ ├── components/ # 用户域专属组件,如 UserAvatar, UserProfileCard
│ │ ├── hooks/ # useUserProfile, useUpdateProfile
│ │ ├── services/ # userApi.ts
│ │ ├── stores/ # useUserStore
│ │ └── types.ts # 域内的类型定义
│ ├── order/ # 订单业务域
│ │ ├── components/
│ │ ├── hooks/
│ │ ├── services/
│ │ └── ...
│ ├── product/ # 商品业务域
│ └── analytics/ # 数据分析域
├── shared/ # 跨业务域共享的通用层
│ ├── components/ # Button, Modal 等
│ ├── hooks/ # useDebounce, useLocalStorage
│ ├── utils/
│ └── types/
└── pages/ # 依然保留页面入口,组合多个 domain
├── dashboard/
└── settings/
关键规则:
- 每个业务域具有完整的内部层级,可以独立开发、独立测试。
- 域之间的通信必须通过显式接口:可以让 A 域导入 B 域的 services 或 hooks,但不允许直接引用 B 域的内部 components 或 stores(除非经过明确的公共 API 导出)。
- 共享模块(shared)应当是业务无关的、通用的能力,不能包含任何业务规则。如果一个组件开始包含“根据用户角色显示不同状态”的逻辑,它应该被移到业务域中,而不是放在 shared 里。
- 数据模型也按域隔离,
user/types.ts只定义用户相关的接口,order/types.ts定义订单模型,避免一坨全局 types 文件不断膨胀。
实际落地时的渐进式策略
对于已经在运行的大型项目,直接按照理想结构重构风险太高。可以采用“绞杀者模式”逐步迁移:
- 先将新的业务需求强制在
domains/xxx下开发,老代码维持原样。 - 当需要修改某个旧功能时,顺势将其从老目录搬入对应的域,并调整分层。
- 最终,旧的扁平目录被逐步清空或废弃。
常见反模式
- 按文件类型分层过度(如所有 hooks 放在全局 hooks/,所有组件放在 components/),当数量爆炸后无法看出业务边界。
- 业务域过大导致内部再次混乱。如果一个域内组件超过 50 个,考虑继续拆分子域(如
user/profile、user/permissions)。 - 域间循环依赖。可以用依赖倒置原则解决:通过共享模块定义接口,然后各域实现接口,或者利用事件总线进行解耦(但需谨慎,避免隐式依赖)。
与组件库设计及公共能力沉淀的关系
清晰的分层和业务域拆分,为组件库设计和公共能力沉淀提供了土壤:
- 从
shared/components中生长出项目内的基础组件库,不断沉淀高频复用的 UI 单元。 - 从各业务域的
hooks/和services/中抽离通用业务封装,沉淀为公共能力,比如usePermission、usePagination。 - 当某个业务模块足够成熟,甚至可以独立为一个 npm 包,被其他项目复用。
合理架构的长期收益远超前期的思考成本。对于任何预期会长期迭代、多人协作的 React 项目,模块分层与业务域拆分不是可选项,而是保证团队效率的基石。