知

模块四 · 性能、调度与架构

第 16 / 18 节19 分钟阅读型

架构与组件模式:从单页到可协作的工程

架构解决的是"改动的影响范围"。这一节讲清目录分层、四种组件模式、Monorepo 与微前端的适用边界,并给出一套可落地的约束清单。

读完这一节,你能做到

  1. 按领域而不是按文件类型组织目录,并给出边界规则。
  2. 在 HOC、render props、复合组件、headless 之间选对一种。
  3. 判断什么时候需要 Monorepo,什么时候需要微前端。
  4. 用可执行的约束(lint 规则、依赖方向)保护架构,而不是靠口头约定。

一、目录:按领域切,不按类型切

按类型组织的典型结构:

src/
├─ components/   (80 个文件)
├─ hooks/
├─ utils/
└─ pages/

问题不在于"看起来乱",而在于改一个需求要在三个顶层目录之间跳,且无法判断某个文件属于哪个业务。

按领域组织:

src/
├─ app/                 # 路由入口与全局配置
├─ features/
│  ├─ order/
│  │  ├─ api.ts         # 该领域的数据访问
│  │  ├─ model.ts       # 类型与状态
│  │  ├─ components/    # 领域内组件
│  │  └─ index.ts       # 对外只导出必要内容
│  └─ user/
├─ shared/
│  ├─ ui/               # 无业务语义的基础组件
│  ├─ lib/              # 工具函数
│  └─ hooks/
└─ pages/               # 只做组合,不写业务逻辑

三条边界规则:

规则说明违反后果
features 之间不能互相 import只能通过 shared 或事件通信领域耦合,改一处影响两处
shared 不能 import features基础层不能依赖业务层循环依赖,构建变慢
pages 只组合,不写逻辑页面是装配层业务散落在路由文件里

这些规则要能被工具检查,否则三个月后一定失效:用 eslint-plugin-boundaries 或 import/no-restricted-paths 把方向写死,CI 里跑。

二、状态分层:架构的核心问题

类型例子存放位置变化频率
UI 状态展开、选中、hover组件内高
表单状态输入值、校验错误组件内 / 表单库高
URL 状态筛选、分页、Tabsearch params中
服务端状态列表、详情请求缓存库中
应用状态登录态、主题、权限Zustand / Context低

架构评审时先画这张表,再问"每一类有没有唯一真相"。常见问题不是"用了什么库",而是同一份数据存在两处:URL 里有 page,store 里也有 page,最后谁都不知道以谁为准。

三、四种组件模式

模式形态适用代价
复合组件<Tabs><Tabs.Item/></Tabs>一组强关联组件需要 Context,嵌套层级隐式
Render Props<List render={item => ...} />需要把内部数据交给调用方渲染嵌套回调可读性差
HOCwithAuth(Component)横切关注点(埋点、权限)props 命名冲突、层级难读
Headless提供状态与行为,不提供样式组件库、设计系统学习成本高,调用方要写样式

选择顺序:能用组合(children)就用组合;需要内部数据就用 render props;横切逻辑用自定义 Hook;只有必须包裹组件时才用 HOC。

HOC 在现代 React 里已经很少用于业务,因为自定义 Hook 能复用同样的逻辑且没有嵌套地狱。它仍然适合"必须在渲染前拦截"的场景,比如错误边界、权限网关。

Headless 组件是设计系统的正确形态:

function useDisclosure(initial = false) {
  const [open, setOpen] = useState(initial);
  return {
    open,
    openIt: () => setOpen(true),
    close: () => setOpen(false),
    toggle: () => setOpen((v) => !v),
    getButtonProps: () => ({
      "aria-expanded": open,
      onClick: () => setOpen((v) => !v),
    }),
    getPanelProps: () => ({ hidden: !open }),
  };
}

它把可访问性属性一并给出,调用方只负责样式。这比"给一个样式写死的 Modal"可复用得多。

四、Monorepo:解决什么问题

问题Monorepo 的解法代价
多个应用共享组件与工具抽 packages/ui、packages/utils构建配置复杂
版本发布不同步统一提交、原子发布需要 CI 支持受影响包构建
跨仓修改成本高一次 PR 改完仓库变大,权限管理更复杂

最小方案是 pnpm workspace:

# pnpm-workspace.yaml
packages:
  - "apps/*"
  - "packages/*"

判断标准:是否有两个以上应用真的在复用同一份代码。只有一个应用时,Monorepo 只是把简单问题复杂化。

五、微前端:最后才考虑

方案隔离级别适用代价
iframe最强完全独立的老系统通信困难、体验割裂
Module Federation运行时共享模块同技术栈、多团队版本兼容与依赖共享复杂
路由分发按路径拆应用团队边界清晰需要统一导航与鉴权

微前端解决的是组织问题,不是技术问题。如果只有一个团队、一个发布节奏,微前端带来的复杂度远大于收益。常见误用是"为了技术先进"引入 Module Federation,最后被 shared 依赖的版本冲突拖住。

判断标准:是否有独立团队需要独立发布。没有,就不要拆。

六、可执行的项目约束

架构靠约定守不住,要靠工具:

约束落地方式
依赖方向ESLint 的 no-restricted-paths
命名与导出index.ts 只导出公共 API
类型共享packages/types 或 OpenAPI 生成
组件分层shared/ui 不允许 import 业务类型
提交规范commitlint + changesets
体积预算CI 里检查 bundle size

再加三条工程习惯:

  1. 每个领域一个 index.ts,外部只能从入口导入,内部文件可自由重构。
  2. API 类型由后端契约生成,不要手写两份。
  3. 把"页面"与"领域"分开:页面负责组装,领域负责逻辑,测试也按这个边界写。

七、一个中型项目的落地结构

src/
├─ app/
│  ├─ router.tsx
│  ├─ providers.tsx        # QueryClient、Theme、Auth
│  └─ layout/
├─ features/
│  ├─ auth/
│  ├─ order/
│  └─ dashboard/
├─ shared/
│  ├─ ui/                  # Button、Modal、Table
│  ├─ lib/                 # http、format、storage
│  └─ hooks/
├─ pages/
│  ├─ order-list/
│  └─ order-detail/
└─ types/

providers.tsx 集中所有全局 Provider,避免在 main.tsx 里堆十几层嵌套;pages 只做"取参数 + 组合领域组件"。

八、常见坑

  1. 按类型分目录,components/ 里堆了上百个文件。
  2. shared 反向依赖 features,形成循环依赖。
  3. 同一份数据同时存在 URL、store 与组件 state 三处。
  4. 为两个页面引入 Monorepo,构建链复杂度翻倍。
  5. 没有独立发布需求就上微前端,shared 依赖天天冲突。
  6. 用 HOC 包一切,DevTools 里组件名全是 WithXxx。
  7. 架构规则只写在文档里,没有 lint 守护。

九、动手任务:自写 → AI 写 → 对比 → 取舍

四步不跳:① 自己写(限时,关掉补全)→ ② 让 AI 写同一题 → ③ 逐条比对 → ④ 说出保留谁的写法。 完整动作与记录模板见《AI 协作与代码评审》。

① 自己写

  1. 把一个按类型组织的目录改成按领域组织,写出三条依赖规则并用 ESLint 落地。
  2. 画一张状态分层表,标出项目里每份数据的位置,找出重复的真相。
  3. 把一个"样式写死的 Modal"改成 headless 组件,补上 aria-* 与键盘关闭。
  4. 评估当前项目是否需要 Monorepo,写出判断依据与不做的理由。

② 让 AI 写同一题

把第 3 题(headless Modal)交给 AI,补一句 无障碍要做到能用键盘完整操作。

③ 对比要盯的点

  • 焦点陷阱、aria-modal、Esc 关闭、滚动锁,它做了几项?通常缺两到三个。
  • open 它做成受控还是内部 state?内部 state 的话调用方就管不了。
  • 它有没有为了省事引入一个组件库?那就不叫 headless 了。

④ 一句话取舍

它给的 Modal 能直接进生产吗?缺的那几项里,哪一条会变成无障碍合规风险?

十、自测

  1. 按领域分目录解决了什么问题?
  2. 四种组件模式的选择顺序是什么?
  3. Monorepo 的判断标准是什么?
  4. 微前端解决的是技术问题还是组织问题?
  5. 架构规则怎样才不会被破坏?

参考答案要点:缩小改动影响范围;组合→render props→Hook→HOC;是否有两个以上应用复用代码;组织问题;用 lint 与 CI 强制。

配套题目

★ 越多越该先背;点题目可看完整答案与深入版。

提示:读完可以顺手做几道对应题目,检验是否真的理解