R53第 1 / 1 课时未开始内容版本 1.0.0 · 核验于 2026-10-26

多文件应用结构

多文件应用结构 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。

学习目标

读完你能把一个多页面 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 持有加载态与竞态保护,页面只消费;
  • 验证:对着每个区块回答「它因为什么原因会改」——答案互不相同的区块,就是拆对了。

动手任务

打开本课实验区:

  1. 为新功能 enrollments 列出文件清单与依赖方向(纸上),再在实验区对应区块补一个极简实现。
  2. 找出「第二个即将重复」的位置(提示:另一个功能也要 request 封装),说明你会等到什么时候才抽取。
  3. 回答:formatPrice 为什么属于 lib 而不是 features/courses?用「修改原因」说清。

常见错误

  • 按类型组织:所有 hooks 一摞、所有组件一摞,功能改动要跨全项目找文件;按功能分组。
  • feature 互相 import 内部:courses 直接引 enrollments/api.js 的内部函数,依赖网成迷宫;公共逻辑下沉 lib,跨功能通信走页面层。
  • 过早抽象:两处相似就抽公共组件,参数按猜的写;等第三次重复拿到两个真实样本再抽。
  • 页面层做重活:pages 里写数据请求和业务分支;页面只组合,重活下沉 feature。

检查与复盘

自查清单:

  • 每个文件能回答「它因为什么原因而改」,且答案单一
  • 依赖方向单向:grep 一个 api 函数只在 api 层出现一次
  • 公共抽取发生在第三次重复,且有两个真实样本支撑

参考资料

动手实验
动手实验
预览
点击「运行」在隔离沙箱中预览
检查题共 1 题,提交后立即显示解析
答案由服务端判定;答错的题会自动进入复习计划。
Q1多选中等(多选,全部选对才算正确)

单文件 App.jsx 已超过 800 行,准备拆分。下列做法合理的有哪些?(多选)

读完这一课了?

标记完成后,工作台会安排接下来的学习。

Command Palette

Search for a command to run...