多文件应用结构
多文件应用结构 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
读完你能把一个多页面 React 应用按「功能模块」组织文件:页面、组件、数据访问、自定义 Hook、测试各就各位,依赖方向单向清晰;掌握「什么时候该拆」的判断,避免为拆而拆。
问题场景
课程管理应用长到第三个页面时,App.jsx 已经 1500 行:三个页面、八个组件、数据请求、工具函数全部挤在一起;更糟的是,请求课程的 fetchCourses 在三个文件里各写了一份,读取鉴权 token 的逻辑有两套,还互不一致。多人协作后没人说得清「改课程列表要动哪几个文件」。结构的目的是让「改动的影响范围」可预测。
核心解释
**按功能组织(feature-based),不按类型(type-based)。**把所有 api.js、所有 components/、所有 hooks/ 各放一摞,是按类型分——一个功能散落四处;按功能分,一个功能的所有文件相邻:
src/
features/
courses/
api.js # fetchCourses / createCourse,只在这一处定义
hooks.js # useCourses:缓存与竞态都收在这层
CourseTable.jsx
tests/CourseTable.test.jsx
enrollments/
api.js
hooks.js
components/ # 跨功能复用的通用组件(Button、Modal)
lib/ # 无业务语义的工具(request 封装、format)
pages/ # 路由级页面,薄:只组合 features
依赖方向单向:pages → features →(components, lib);feature 之间不互相 import 内部文件,公共部分下沉到 components / lib。这条规则让「改 api.js 影响谁」变成一个可以 grep 的问题。数据访问层的形状也由此确定——只做获取与转换,不掺界面逻辑:
// features/courses/api.js —— 只做数据获取与形状转换
export async function fetchCourses() {
const res = await request("/api/courses");
return res.map((r) => ({ id: r.id, title: r.name, hours: r.hourCount }));
}
**拆分的时机:第三次重复时。**第一份直接写,第二份复制,第三份出现才抽公共——提前抽象的参数往往靠猜,返工成本高于重复成本。判断一段代码该不该独立成文件的实操标准:修改原因。api.js 因为后端改版而改,组件因为交互改版而改,修改原因不同就该分家;修改原因一致的,挤在一个文件里反而好维护。
测试与被测文件就近,放在 features//tests/,找测试不用翻目录;跨功能的共享夹具才集中。
示例拆解
实验区把上述结构映射进一个可运行的单文件,用注释分隔出五个「虚拟文件」:lib/request、lib/format、features/courses/api、features/courses/hooks、components/PriceTag、pages/CoursesPage,与真实工程一一对应:
- request 封装统一模拟鉴权与错误处理——全应用唯一一份;
- courses 的 api 层只做获取与字段转换(hourCount → hours,priceCent → price);
- useCourses 持有加载态与竞态保护,页面只消费;
- 验证:对着每个区块回答「它因为什么原因会改」——答案互不相同的区块,就是拆对了。
动手任务
打开本课实验区:
- 为新功能 enrollments 列出文件清单与依赖方向(纸上),再在实验区对应区块补一个极简实现。
- 找出「第二个即将重复」的位置(提示:另一个功能也要 request 封装),说明你会等到什么时候才抽取。
- 回答:formatPrice 为什么属于 lib 而不是 features/courses?用「修改原因」说清。
常见错误
- 按类型组织:所有 hooks 一摞、所有组件一摞,功能改动要跨全项目找文件;按功能分组。
- feature 互相 import 内部:courses 直接引 enrollments/api.js 的内部函数,依赖网成迷宫;公共逻辑下沉 lib,跨功能通信走页面层。
- 过早抽象:两处相似就抽公共组件,参数按猜的写;等第三次重复拿到两个真实样本再抽。
- 页面层做重活:pages 里写数据请求和业务分支;页面只组合,重活下沉 feature。
检查与复盘
自查清单:
- 每个文件能回答「它因为什么原因而改」,且答案单一
- 依赖方向单向:grep 一个 api 函数只在 api 层出现一次
- 公共抽取发生在第三次重复,且有两个真实样本支撑
参考资料
- Next.js 文档:项目结构 —— 功能组织的工程参照
- React 官方文档:管理状态 —— 状态与逻辑在结构中的落位
- React 官方文档:reducer 与 context 扩展 —— 逻辑从组件抽出的形态
读完这一课了?
标记完成后,工作台会安排接下来的学习。