返回笔记列表
01 · NOTES / WebGIS 开发

第 09 节 · 项目组织

2026 年 9 月 1 日

第 09 节 · 项目组织

📌 版本信息:React 19.x(2026-08-29 核对) 📚 来源:React 中文文档 · 用 Context 与组合扩展 | 社区共识结构(bulletproof-react 思想的精简版)

一、这一节的目标

  1. 掌握 React 项目的标准目录结构(本课程后续所有项目统一用它)
  2. 掌握组件拆分的判断标准:什么时候该拆、拆到多细
  3. 初识自定义 Hook:把"有状态的逻辑"从组件里抽出来
  4. 完成"拆分一个页面"练习——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/              纯工具函数

三条纪律:

  1. pages 只组装:页面组件把子组件拼起来、接住路由参数,不做具体 UI 细节
  2. components 不带业务:CityCard 不知道"收藏"是什么——它只显示收到的 props;要不要收藏由 pages/features 决定
  3. 文件按职责归类,不按文件类型:不要建 styles/ hooks/ components/ 三大筐一锅烩,功能内聚优先

💡 这个结构是 bulletproof-react 等社区共识的精简版,够用到中型项目;第 13 模块后端(FastAPI)的分层(routers/models/schemas)思想完全同构——"按职责分层"是全栈通用语言


三、组件拆分的判断标准

拆分信号(出现两条就该拆):

  1. 重复:同一段 JSX 出现第二次 → 拆成组件传 props
  2. 过长:一个组件超过 ~150 行 / 一个 return 里超过 3 层嵌套 → 按视觉块拆
  3. 独立状态:某块 UI 有自己的 state(弹窗开关、输入草稿)→ 拆出去收拢状态
  4. 职责混杂:一个组件既管请求又管筛选又管渲染 → 按职责拆

拆分反信号(别过度拆):

  • 只被用一次、且只有几行 → 内联即可
  • 拆完要传 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

步骤:

  1. 打开 src/Monolith.jsx——一个 300 行的"灾难页面":地震面板(筛选+列表+统计+弹窗)全部糊在一个组件里(我故意写乱的,和很多"AI 一次生成"的产物一样)
  2. 你的任务:按第二节的标准把它拆成标准结构(目标结构在 TODO 里给出):
    • pages/QuakePage.jsx(组装层)
    • components/FilterPanel.jsxQuakeList.jsxStatsBar.jsxDetailModal.jsx
    • hooks/useFilteredQuakes.js(筛选逻辑抽成 Hook)
    • utils/format.js(时间/震级格式化)
  3. 拆完后 npm run dev 功能必须与原来完全一致
  4. 每拆一块 commit 一次(Git 好习惯:小步提交,坏了随时回滚)

通关标准:

  • Monolith.jsx 删除,功能零丢失
  • 最长文件 ≤ 100 行
  • useFilteredQuakes 被两个组件复用(统计条也用它)
  • 能说出"哪些该拆、哪些不该拆"各举一例

六、自测题

  1. pages 和 components 的职责分界?
  2. 四个"该拆"的信号?两个"不该拆"的反信号?
  3. 自定义 Hook 的命名规则与能力(普通函数能不能调 useState)?
  4. 为什么说"文件按职责归类,不按文件类型"?
  5. 一个组件要传 8 个 props 才能工作,说明什么?

参考答案

  1. pages 只做组装与路由对接;components 是可复用的纯展示件(props 进、UI 出,不知道业务)。
  2. 信号:重复/过长/独立状态/职责混杂。反信号:只用一次且很短;拆完要传一堆 props。
  3. 必须以 use 开头;只有自定义 Hook(或组件)里才能调用其他 Hook——普通函数调用 Hook 会破坏 React 的状态机制。
  4. 按类型归类(所有组件一筐)会让相关代码散落各处;按职责归类让"一个功能的所有代码"在一起,改需求只动一个文件夹。
  5. 拆分边界错了(或该合并数据/用 Context)——先重新审视职责划分,而不是硬传。

七、下一步

工程化就位 → 第 10 节:组件库 Ant Design 上手,用现成组件武装你的项目,然后正式开工 C-03a。

TAGSweb