跳到正文
前端知识库
项目亮点

项目三:React Hooks 与组件库工程化

从业务 Hooks 抽象到多格式构建、文档示例、测试和版本发布,整理一个可复用 React 基建项目。

9 分钟React · Hooks · 组件库 · TypeScript · Jest · Monorepo · UMD · ESM · CI/CD

项目三:React Hooks 与组件库工程化

资料中的项目是一个带 Demo 官网的 React Hooks 库,包含 TypeScript、Jest、gulp/webpack 构建、UMD 发布、CHANGELOG 和 CI。本文将它改写成可实际交付的公共基建项目,并补充 Hooks 的闭包、取消、竞态和 API 兼容边界。组件库的通用规范另见 前端组件库工程化

1. 背景与目标

多个 React 业务会重复实现请求状态、分页、轮询、事件监听、媒体查询、键盘交互和表单逻辑。复制代码短期快,长期会出现:状态语义不一致、卸载后仍更新、请求竞态覆盖新数据、测试难以复用、修复无法同步到其他项目。

项目目标不是“封装越多越好”,而是建立一组边界清晰、可测试、可发布的 Hooks,并让消费者知道每个 Hook 的生命周期和失败语义:

  • Hook 只封装稳定的状态/副作用协议,不把某个页面的业务字段硬编码进去。
  • 返回值和错误语义稳定,受控参数与默认参数不在运行中无提示切换。
  • 网络请求支持取消、去重、重试和过期结果丢弃。
  • 文档 Demo、类型声明、测试和发布包来自同一提交。
  • 使用者可以按包或入口按需引入,不因为一个 Hook 引入整套运行时。

2. 包与仓库结构

packages/
  hooks-core/       纯状态机和公共类型
  hooks-dom/        浏览器事件、媒体查询和 DOM 适配器
  hooks-request/    请求状态、缓存、取消和重试
  react-hooks/      React 绑定层
  docs/             Demo、API 和迁移文档
tests/              fixture、契约和浏览器交互测试

可以使用 pnpm workspace/monorepo 管理包,但每个包都要声明自己的依赖和公开入口。构建同时产出 ESM、CJS/UMD(只有确有兼容需求时)和 .d.ts;CSS 或 polyfill 等副作用在 package.jsonsideEffects 中准确声明。

{
  "name": "@acme/react-hooks",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js",
      "require": "./dist/index.cjs"
    },
    "./use-request": {
      "types": "./dist/use-request.d.ts",
      "import": "./dist/use-request.js",
      "require": "./dist/use-request.cjs"
    }
  },
  "peerDependencies": { "react": ">=18" },
  "sideEffects": false
}

不要让消费者依赖 src/internal 深层路径;需要实验能力时使用带明确稳定性标记的入口。构建工具(gulp、webpack、Vite 等)是实现细节,面试中应能说清入口、依赖图、产物和验证,而不是只报工具名称。

3. Hook 设计方法

先写状态机和不变量,再设计函数签名。例如请求 Hook:

idle -> loading -> success
             \-> error(retryable | terminal)
loading -> cancelled
success -> loading (manual refetch / stale revalidation)

一个可解释的接口可以是:

type RequestState<T> = {
  data?: T
  error?: unknown
  status: 'idle' | 'loading' | 'success' | 'error' | 'cancelled'
}

type RequestOptions<T> = {
  key: string
  fetcher: (signal: AbortSignal) => Promise<T>
  enabled?: boolean
  retry?: number
  retryDelay?: (attempt: number) => number
  onError?: (error: unknown) => void
}

function useRequest<T>(options: RequestOptions<T>): RequestState<T> & {
  refetch(): void
  cancel(): void
} {
  // 实现中用 ref 保存当前请求和最新回调,cleanup 时 abort。
  throw new Error('example contract')
}

实现时要注意:

  1. 每次请求生成递增版本或 requestId,只有当前请求能提交结果;旧响应到达时丢弃。
  2. AbortController 在依赖变化和组件卸载时调用;取消是预期分支,不应触发错误提示或自动重试。
  3. 重试只针对明确的幂等/临时错误,采用上限和退避,不能让页面卸载后的定时器继续运行。
  4. fetcheronError 等回调可能变化,用 ref 保存最新值,避免 stale closure;依赖数组仍要表达真正影响请求的参数。
  5. enabled=false 时不发请求;从禁用切换到启用的行为写进文档和测试。

3.1 插件式请求能力

资料提到错误重试、轮询、自动/手动请求、防抖和节流。不要把这些开关堆成互相冲突的布尔值,可拆为可组合策略:

type RequestPlugin<T> = {
  onStart?(ctx: RequestContext<T>): void
  onSuccess?(ctx: RequestContext<T>): void
  onError?(ctx: RequestContext<T>): 'retry' | 'stop' | void
  onSettled?(ctx: RequestContext<T>): void
}

轮询需要处理页面隐藏、网络离线和并发:下一次轮询必须在前一次完成后计时,或明确允许并行并设置上限;回到页面时可重新验证。防抖适合搜索提交,节流适合滚动/resize,二者都要在卸载时清理定时器并保留最后一次参数的语义。

3.2 浏览器与 DOM Hook

useEventListeneruseIntersectionObserveruseMediaQuery 等 Hook 应把能力检测、监听器选项和清理写入契约:

  • SSR 阶段不能访问 window/document,返回明确的初始值。
  • passive、捕获阶段和目标元素变化需有测试;元素被替换时解绑旧监听器。
  • Observer/Worker/对象 URL 等资源在 cleanup 中释放。
  • 事件回调访问最新状态时避免每次渲染重绑,或清楚说明重绑成本。

4. React 运行模型与常见坑

Hooks 按调用顺序挂在 Fiber 节点的链表上,条件调用会破坏对应关系;“Rules of Hooks”是运行时数据结构约束,不只是风格规范。闭包使每次渲染都捕获自己的值,因此:

  • setState(value) 连续调用可能基于同一个旧快照;需要累积时使用函数式更新。
  • Effect 中的请求、定时器和事件监听器必须处理依赖、取消和 cleanup。
  • useMemo/useCallback 不是万能性能开关,只有在计算昂贵或引用稳定能减少实际渲染时才有意义。
  • Context 值对象每次创建会让所有消费者重渲染,必要时拆分上下文或按选择器订阅。
  • Strict Mode 开发环境可能重复执行某些初始化,副作用必须幂等。

预测资料还提到 React 19 的 Actions、useTransitionuseOptimisticuseFormStatususe 和错误回调。项目中采用这些能力时要锁定 React 版本、说明是否需要渐进降级,并用 onRecoverableError 等入口关联监控;不要把实验 API 写成所有消费者都可用的硬依赖。状态库(Redux、Zustand、Jotai、Context 等)按状态所有权、调试、异步和订阅粒度选择,不能用“某库一定更快”作为结论。

深入阅读:React Hooks 理解React 状态与性能React 组件性能优化

5. 类型、文档和 Demo

公共 Hook 的类型要表达输入约束和返回状态,而不是用 any 逃避不确定性:

  • 泛型参数应能从 fetcher 或初始值推断;错误类型若无法约束,文档说明 unknown 的处理方式。
  • 可选配置有默认值,默认行为写入 API 表格;破坏性修改要有迁移示例。
  • Demo 覆盖受控/非受控、加载、错误、取消、空值和键盘交互,不只展示成功路径。
  • 文档站点和 npm 包从同一版本构建,示例导入公开入口,避免官网能运行而发布包缺文件。
  • 在线 CodeSandbox 等第三方环境只能作为演示,不能替代本地 CI 和锁定依赖的回归测试。

6. 测试策略

测试按风险分层:

层级 覆盖内容 关键 fixture
纯函数/状态机 重试次数、退避、状态转移、去重 可控时钟、固定响应序列
Hook 渲染测试 首次加载、依赖变化、卸载、Strict Mode mock fetcher、AbortSignal 断言
组件交互 键盘、焦点、错误和异步反馈 可访问定位器、稳定 DOM
包契约 ESM/CJS/UMD、类型声明、exports 干净临时消费者项目
浏览器 E2E 文档导航、Demo、发布包真实导入 隔离网络和版本 fixture

每个异步测试都等待可观察状态,而不是 sleep 固定时间。故意让 fetcher 延迟、拒绝、乱序和取消,验证旧结果不会覆盖新结果。可以用 mutation 或小缺陷注入确认测试真正能阻止回归。

7. 构建、发布和版本治理

提交 -> 类型检查/Lint -> 单元/交互测试 -> 构建 ESM/CJS/UMD + d.ts
      -> 包内容/exports 检查 -> 生成 changelog -> 预发布验证
      -> npm/内部仓库发布 -> 文档/CDN 同步 -> 监控与回滚

发布要点:

  1. 使用语义化版本;删除 Hook、改变默认重试或改变返回状态属于破坏性变更,应提供迁移窗口。
  2. 同一个版本号不能覆盖不同产物;记录 commit、构建器版本和依赖锁文件。
  3. UMD 只在确有旧系统或 CDN 需求时保留,默认优先 ESM tree-shaking;检查重复 React、全局变量和 sourcemap 泄露。
  4. CHANGELOG 自动化只能生成草稿,最终内容要人工确认影响范围和迁移步骤。
  5. 发布失败或消费者安装失败时,保留上一稳定版本和撤回入口,不依赖“重新发一次同版本”。

8. 失败边界与复盘

失败 根因示例 预防/止血
旧请求覆盖新请求 没有 requestId 或 abort 当前请求版本校验、取消和乱序测试
卸载后 setState Effect cleanup 缺失 统一资源管理 helper、卸载测试
生产包缺少类型/样式 exports 或 sideEffects 配置错误 干净消费者安装检查、产物清单
Hook API 破坏消费者 默认值/返回形状无版本治理 语义化版本、弃用警告和迁移文档
官网 Demo 与包不一致 文档和包使用不同提交 同一 CI 产出、锁定依赖和发布 smoke test
自动重试放大故障 未区分取消、429 和业务错误 幂等判断、退避、上限、熔断开关

复盘时记录“哪个不变量被破坏、哪条测试没有覆盖、怎样在发布前捕获”,不要只写“加强测试”。

9. 指标与证据

项目价值可从以下维度证明:

维度 指标定义 证据来源
复用 真实消费者数、重复逻辑删除量、公开 API 使用率 依赖清单、代码搜索、迁移 PR
可靠性 Hook 相关回归缺陷、取消/重试成功率、flaky 测试率 CI、错误监控、测试报告
交付 从提交到发布耗时、构建失败原因、回滚次数 CI/CD 日志
产物 ESM/UMD 包大小、重复依赖、类型检查通过率 bundle 分析和消费者 smoke test
体验 Demo 加载、文档搜索/反馈、组件可访问性缺陷 浏览器 trace、反馈和审计

任何“复用提升”“首屏提升”等数字都要带版本、样本和测量方法;没有记录就写待验证,不引用资料中的示例百分比。

10. 面试表达与追问

三分钟版本

团队多个 React 项目重复实现请求、轮询和 DOM 逻辑,复制代码导致取消、竞态和版本问题。
我负责 Hooks API、请求状态机、测试和发布链路,按 hooks-core、浏览器适配和 request 包拆边界,产出 ESM/CJS/类型声明并提供可运行 Demo。
请求 Hook 用 requestId/AbortController 丢弃旧响应,重试只覆盖幂等临时错误;Effect cleanup、SSR 能力检测和 Strict Mode 都有回归用例。
CI 从类型/Lint 到包内容和消费者安装,发布带 changelog、版本和回滚入口;结果用真实消费者、缺陷和构建记录证明。

高频追问

追问 回答要点 深入阅读
为什么要封装 Hook,而不是复制工具函数? Hook 能把状态、生命周期和副作用契约一起复用;纯函数仍应抽到 core,避免把所有逻辑绑在 React React Hooks
如何处理请求竞态? 每次请求有 ID/版本,只有当前请求提交;切换和卸载 abort;测试乱序响应 AI 前端工程
防抖和节流放在哪里? 纯函数层可复用,Hook 层负责生命周期和最新回调;说明输入/尾调用/取消语义 JavaScript 高频追问
为什么要出 UMD? 只有旧系统/CDN 需要;现代应用优先 ESM,比较包体、全局污染和缓存粒度 组件库工程化
如何保证发布包可用? exports、d.ts、ESM/CJS/UMD、peerDependencies 和 CSS 副作用做干净消费者检查 组件库工程化
React 19 能直接使用吗? 先锁版本和消费者范围,给渐进降级;错误回调和新 Hook 要有版本/测试边界 React 面试真题补充

11. 交付清单

  • 每个 Hook 有状态机、参数/返回契约和失败语义。
  • 取消、竞态、卸载、SSR、Strict Mode 和高频事件有测试。
  • 包边界、exports、类型声明、peerDependencies 和副作用经过消费者验证。
  • 文档 Demo 与发布包来自同一提交,包含错误/空态/键盘示例。
  • CI 有类型、Lint、单元、交互、构建和包内容检查,发布可回滚。
  • 结果指标带版本、样本、基线和测量方法。

来源:高级前端亮点项目.pdf 第 25-27 页(React Hooks 库、TypeScript、Jest、gulp/webpack、UMD、Dumi、Demo 和 CI 发布);高频真题解析与9月考点预测上.pdf 第 15-16 页(Hooks/Fiber、状态管理和 React 19);高频真题解析与9月考点预测中.pdf 第 2-9 页(防抖/节流、主题与性能指标)。