# 前端迭代防回归经验

## 适用范围

- **完全适用：** 组件化单页应用（React、Vue、Svelte、Solid、Angular），配合客户端路由和请求缓存库。换框架时只需换成对应写法，例如 children / render prop 对应插槽或内容投影，"渲染时计算"对应 computed / derived。
- **大部分适用：** 服务端渲染和全栈框架（Next.js、Nuxt、Remix、SvelteKit）以及微前端。在服务端渲染和全栈框架里，"一个路由一个页面"本来就是框架约定。"避免请求串行"则改为借助路由 loader 或服务端组件并行取数，交互状态放在客户端组件里。
- **部分适用：** 服务端模板加少量交互（传统多页应用、htmx 等）和原生移动应用。主要适用诊断方法、结构原则和流程纪律，状态管理和代码拆分的条目基本不涉及。
- **与架构无关：** 先度量再重构、共享模块不按调用方分支、纯逻辑可单测、前后端规则对齐、重构与行为变更分开提交、测试失败找根因。

## 一、先诊断，再动手

- **先确认问题是什么。** "过度组件化、嵌套太深"只是一个假设，要先用数据检验。这次实测的结果相反：组件并不多，问题是少数几个上帝组件把路由、弹窗、表单和数据请求都揉在了一起。
- **先格式化，再度量。** 很多行超长的代码按行数统计会严重低估复杂度。先统一格式，再看字符数、JSX 嵌套深度、hooks 数量、props 数量和 prop 传递层数。
- **动手前先固定基线。** 类型检查、单元测试、端到端测试和关键页面截图都要先跑通并留存。之后每一步都和这条基线比较。
- **清理前先确认用途。** 只用一两次的小包装组件可能承担着真实职责，比如充当 Provider 边界，或者靠 key 重置内部状态。确认它不承担职责后再删。

## 二、结构规则

1. **布局由路由决定，不由组件状态决定。** 不要在一个组件里用 `page === 'x' &&` 或 `mode` 状态切换整块页面。如果一组 Tab 代表的是页面级内容，Tab 的值就应该来自 URL，切换 Tab 就是导航。
2. **一个路由对应一个页面。** 页面负责取数、持有本页的弹窗状态、组合组件。页面里不放可复用的展示块。
3. **布局、页面、通用组件、业务组件分开存放。** 和业务无关的通用封装集中在一个目录。第三方原样复制进来的组件保持上游格式，不纳入格式化，方便以后同步上游更新。
4. **共享组件不按角色或模式分支。** 发现组件里有 `isAdmin`、`mode` 这类布尔值，同时控制接口路径、可见功能和请求字段，就应该拆。拆法是：

   - 接口路径作为数据传入，组件不根据它做判断；
   - 各角色不同的部分（筛选项、行操作、额外卡片、头部按钮）用 children 或 render prop 由调用方传入；
   - 各操作需要哪些输入，由调用方声明成一份配置，不让组件自己推断。
5. **用组合代替一堆回调和布尔 props。** 卡片的操作按钮由调用方以 children 传入，不为每种操作单独加一个 `onXxx` 或 `showXxx`。
6. **一种实体一个编辑器。** 不要用 `kind` 字段在一个大组件里分支处理多种实体。多个编辑器需要相同的外壳时，抽一个外壳组件共用。同时去掉 `as unknown as ...` 这类类型擦除，让每个编辑器使用真实的类型。
7. **共享数据从缓存层读取。** 多个页面都要用的查询封装成 hook，各处直接读缓存，不要逐层用 props 传下去。全局 Provider 只放真正全局的东西。

## 三、状态与逻辑

- **能在渲染时算出来的值，就不要用 effect 去同步。** 例如"已选中的项只能是当前筛选结果中的项"，渲染时直接求交集即可，不需要 `useEffect` 去裁剪。
- **需要整体重置状态时，换 key 让组件重新挂载。** 比用 effect 监听依赖再逐个重置字段更可靠。
- **纯逻辑移出组件并写单元测试。** 列表增删、校验、提交数据的组装、"是否发生变更"这类判断都是纯函数，单独测试成本很低，收益很高。
- **前端预测后端的判断时，规则要和后端一致。** 这次补测试时发现：前端判断"是否需要额外确认"比较的字段比后端少，结果用户在前端看不到提示，提交后却被后端拒绝。凡是前端提前推断后端结论的逻辑，都要对照后端的规则逐项核对，并写测试固定下来。

## 四、性能

- **代码拆分的粒度会影响请求的并发。** 按页面拆 chunk 后，页面代码要等布局加载完才开始下载，页面的首批请求就排到了布局请求之后，错过了共享的令牌续期，多出一次重复请求。按用户区域（比如按角色）拆 chunk，布局和页面会一起到达，首批请求重新并发。
- **改动拆分方式后，查看网络时间线确认效果。** 不要假设"拆得越细越快"。

## 五、流程纪律

- **小步提交，每一步都能单独验证。** 格式化、移动文件、重构、修缺陷分开提交。行为不变的重构不要和行为变更混在同一个提交里。
- **修缺陷时先写一条会失败的测试复现问题，再修代码，并单独提交。**
- **测试失败时找根因，不改测试去迁就实现。** 这次一条并发相关的测试失败，暴露的正是上面那个请求串行的问题。
- **不留兼容层。** 用 codemod 一次性替换全部调用点，删除旧组件，避免新旧两条路径并存。
- **视觉回归要逐个解释差异。** 每个像素差异都要找到原因，确认是随机端口、实时数据这类无关因素，才能判定为无变化。
- **记录有意的行为变化。** 例如 URL 变了、错误改在别处显示，都要在文档里写明，不要只写"无变化"。

## 六、需要警惕的信号

出现以下任意一条，就该停下来拆分或重新设计：

- 组件里出现 `xxx === 'a' && <整块布局/>` 的多路分支；
- 一个组件里有超过 5 个 useState，或者同时持有多个弹窗的开关状态；
- 布尔 props 同时影响接口路径、可见功能和请求字段；
- 一个组件用 `kind` 或 `type` 字段区分多种实体，并伴随类型擦除；
- 用 `useEffect` 让一份 state 跟着另一份 state 同步；
- 同一段选项映射或格式化代码在两处以上复制；
- 前端判断规则和后端各写一份，但没有测试把两者绑在一起；
- 新增懒加载以后，首屏请求的时间线变成了串行。