01 · NOTES / WebGIS 开发
第 09 节 · 项目组织
2026 年 9 月 1 日
第 09 节 · 项目组织
📌 版本信息:React 19.x(2026-08-29 核对) 📚 来源:React 中文文档 · 用 Context 与组合扩展 | 社区共识结构(bulletproof-react 思想的精简版)
一、这一节的目标
- 掌握 React 项目的标准目录结构(本课程后续所有项目统一用它)
- 掌握组件拆分的判断标准:什么时候该拆、拆到多细
- 初识自定义 Hook:把"有状态的逻辑"从组件里抽出来
- 完成"拆分一个页面"练习——C-03a 开工前的最后一课
二、标准目录结构
src/
├── main.jsx 入口(永远只有 4 行)
├── App.jsx 路由表/最外层布局
├── index.css 全局样式(或 Tailwind 入口)
├── pages/ 页面级组件(与路由一一对应,只做"组装")
│ ├── HomePage.jsx
│ └── CityPage.jsx
├── components/ 可复用展示组件(不含业务数据,props 进 UI 出)
│ ├── CityCard.jsx
│ └── EmptyState.jsx
├── features/ 业务功能块(大项目用:每个功能一个文件夹)
│ └── favorites/
│ ├── FavoritesProvider.jsx (状态)
│ └── FavoriteButton.jsx (交互)
├── context/ 全局 Context(theme / user / map-instance)
├── hooks/ 自定义 Hook(useLocalStorage、useDebounce…)
├── data/ 静态数据/类型定义(第 13 模块起变成 api/ 层)
└── utils/ 纯工具函数
三条纪律:
- pages 只组装:页面组件把子组件拼起来、接住路由参数,不做具体 UI 细节
- components 不带业务:CityCard 不知道"收藏"是什么——它只显示收到的 props;要不要收藏由 pages/features 决定
- 文件按职责归类,不按文件类型:不要建
styles/ hooks/ components/三大筐一锅烩,功能内聚优先
💡 这个结构是 bulletproof-react 等社区共识的精简版,够用到中型项目;第 13 模块后端(FastAPI)的分层(routers/models/schemas)思想完全同构——"按职责分层"是全栈通用语言。
三、组件拆分的判断标准
拆分信号(出现两条就该拆):
- 重复:同一段 JSX 出现第二次 → 拆成组件传 props
- 过长:一个组件超过 ~150 行 / 一个 return 里超过 3 层嵌套 → 按视觉块拆
- 独立状态:某块 UI 有自己的 state(弹窗开关、输入草稿)→ 拆出去收拢状态
- 职责混杂:一个组件既管请求又管筛选又管渲染 → 按职责拆
拆分反信号(别过度拆):
- 只被用一次、且只有几行 → 内联即可
- 拆完要传 8 个 props 才能工作 → 拆错了边界(可能该合并数据或用 Context)
拆分套路(从第 14 节待办练习逆向):一个"页面"天然分三块——数据获取/状态区、操作逻辑区、渲染区;渲染区再按视觉块(工具栏/列表/统计条)各成一个组件。
四、自定义 Hook:逻辑的函数化复用
规则:use 开头的函数里可以调用其他 Hook。 把"一段有状态的逻辑"从组件里抽走,组件只剩组装:
// hooks/useLocalStorage.js —— 一个 10 行但全项目通用的 Hook
import { useState } from 'react';
export function useLocalStorage(key, initial) {
const [value, setValue] = useState(() => {
try {
return JSON.parse(localStorage.getItem(key)) ?? initial;
} catch {
return initial;
}
});
const set = (v) => {
setValue(v);
localStorage.setItem(key, JSON.stringify(v));
};
return [value, set];
}
// 用法:和 useState 一模一样,但自动持久化
const [theme, setTheme] = useLocalStorage('theme', 'light');
再举两个马上会用的:
// hooks/useDebounce.js:输入防抖(搜索框 300ms 才触发查询)
// hooks/useDocumentTitle.js:页面标题随路由/数据变化
⚠️ 边界:只在"逻辑要复用"或"组件太臃肿"时抽 Hook;只在单组件用一次的逻辑,留在组件里反而清晰。
五、动手跟练:09 · 拆分一个页面
配套文件夹:03-react-nextjs/examples/09-拆分一个页面/(npm i && npm run dev)
步骤:
- 打开
src/Monolith.jsx——一个 300 行的"灾难页面":地震面板(筛选+列表+统计+弹窗)全部糊在一个组件里(我故意写乱的,和很多"AI 一次生成"的产物一样) - 你的任务:按第二节的标准把它拆成标准结构(目标结构在 TODO 里给出):
pages/QuakePage.jsx(组装层)components/FilterPanel.jsx、QuakeList.jsx、StatsBar.jsx、DetailModal.jsxhooks/useFilteredQuakes.js(筛选逻辑抽成 Hook)utils/format.js(时间/震级格式化)
- 拆完后
npm run dev功能必须与原来完全一致 - 每拆一块 commit 一次(Git 好习惯:小步提交,坏了随时回滚)
通关标准:
- Monolith.jsx 删除,功能零丢失
- 最长文件 ≤ 100 行
- useFilteredQuakes 被两个组件复用(统计条也用它)
- 能说出"哪些该拆、哪些不该拆"各举一例
六、自测题
- pages 和 components 的职责分界?
- 四个"该拆"的信号?两个"不该拆"的反信号?
- 自定义 Hook 的命名规则与能力(普通函数能不能调 useState)?
- 为什么说"文件按职责归类,不按文件类型"?
- 一个组件要传 8 个 props 才能工作,说明什么?
参考答案
- pages 只做组装与路由对接;components 是可复用的纯展示件(props 进、UI 出,不知道业务)。
- 信号:重复/过长/独立状态/职责混杂。反信号:只用一次且很短;拆完要传一堆 props。
- 必须以 use 开头;只有自定义 Hook(或组件)里才能调用其他 Hook——普通函数调用 Hook 会破坏 React 的状态机制。
- 按类型归类(所有组件一筐)会让相关代码散落各处;按职责归类让"一个功能的所有代码"在一起,改需求只动一个文件夹。
- 拆分边界错了(或该合并数据/用 Context)——先重新审视职责划分,而不是硬传。
七、下一步
工程化就位 → 第 10 节:组件库 Ant Design 上手,用现成组件武装你的项目,然后正式开工 C-03a。
TAGSweb