前端迭代防回归经验
适用范围
- 完全适用: 组件化单页应用(React、Vue、Svelte、Solid、Angular),配合客户端路由和请求缓存库。换框架时只需换成对应写法,例如 children / render prop 对应插槽或内容投影,"渲染时计算"对应 computed / derived。
- 大部分适用: 服务端渲染和全栈框架(Next.js、Nuxt、Remix、SvelteKit)以及微前端。在服务端渲染和全栈框架里,"一个路由一个页面"本来就是框架约定。"避免请求串行"则改为借助路由 loader 或服务端组件并行取数,交互状态放在客户端组件里。
- 部分适用: 服务端模板加少量交互(传统多页应用、htmx 等)和原生移动应用。主要适用诊断方法、结构原则和流程纪律,状态管理和代码拆分的条目基本不涉及。
- 与架构无关: 先度量再重构、共享模块不按调用方分支、纯逻辑可单测、前后端规则对齐、重构与行为变更分开提交、测试失败找根因。
一、先诊断,再动手
- 先确认问题是什么。 "过度组件化、嵌套太深"只是一个假设,要先用数据检验。这次实测的结果相反:组件并不多,问题是少数几个上帝组件把路由、弹窗、表单和数据请求都揉在了一起。
- 先格式化,再度量。 很多行超长的代码按行数统计会严重低估复杂度。先统一格式,再看字符数、JSX 嵌套深度、hooks 数量、props 数量和 prop 传递层数。
- 动手前先固定基线。 类型检查、单元测试、端到端测试和关键页面截图都要先跑通并留存。之后每一步都和这条基线比较。
- 清理前先确认用途。 只用一两次的小包装组件可能承担着真实职责,比如充当 Provider 边界,或者靠 key 重置内部状态。确认它不承担职责后再删。
二、结构规则
-
布局由路由决定,不由组件状态决定。 不要在一个组件里用
page === 'x' &&或mode状态切换整块页面。如果一组 Tab 代表的是页面级内容,Tab 的值就应该来自 URL,切换 Tab 就是导航。 -
一个路由对应一个页面。 页面负责取数、持有本页的弹窗状态、组合组件。页面里不放可复用的展示块。
-
布局、页面、通用组件、业务组件分开存放。 和业务无关的通用封装集中在一个目录。第三方原样复制进来的组件保持上游格式,不纳入格式化,方便以后同步上游更新。
-
共享组件不按角色或模式分支。 发现组件里有
isAdmin、mode这类布尔值,同时控制接口路径、可见功能和请求字段,就应该拆。拆法是:- 接口路径作为数据传入,组件不根据它做判断;
- 各角色不同的部分(筛选项、行操作、额外卡片、头部按钮)用 children 或 render prop 由调用方传入;
- 各操作需要哪些输入,由调用方声明成一份配置,不让组件自己推断。
-
用组合代替一堆回调和布尔 props。 卡片的操作按钮由调用方以 children 传入,不为每种操作单独加一个
onXxx或showXxx。 -
一种实体一个编辑器。 不要用
kind字段在一个大组件里分支处理多种实体。多个编辑器需要相同的外壳时,抽一个外壳组件共用。同时去掉as unknown as ...这类类型擦除,让每个编辑器使用真实的类型。 -
共享数据从缓存层读取。 多个页面都要用的查询封装成 hook,各处直接读缓存,不要逐层用 props 传下去。全局 Provider 只放真正全局的东西。
三、状态与逻辑
- 能在渲染时算出来的值,就不要用 effect 去同步。 例如"已选中的项只能是当前筛选结果中的项",渲染时直接求交集即可,不需要
useEffect去裁剪。 - 需要整体重置状态时,换 key 让组件重新挂载。 比用 effect 监听依赖再逐个重置字段更可靠。
- 纯逻辑移出组件并写单元测试。 列表增删、校验、提交数据的组装、"是否发生变更"这类判断都是纯函数,单独测试成本很低,收益很高。
- 前端预测后端的判断时,规则要和后端一致。 这次补测试时发现:前端判断"是否需要额外确认"比较的字段比后端少,结果用户在前端看不到提示,提交后却被后端拒绝。凡是前端提前推断后端结论的逻辑,都要对照后端的规则逐项核对,并写测试固定下来。
四、性能
- 代码拆分的粒度会影响请求的并发。 按页面拆 chunk 后,页面代码要等布局加载完才开始下载,页面的首批请求就排到了布局请求之后,错过了共享的令牌续期,多出一次重复请求。按用户区域(比如按角色)拆 chunk,布局和页面会一起到达,首批请求重新并发。
- 改动拆分方式后,查看网络时间线确认效果。 不要假设"拆得越细越快"。
五、流程纪律
- 小步提交,每一步都能单独验证。 格式化、移动文件、重构、修缺陷分开提交。行为不变的重构不要和行为变更混在同一个提交里。
- 修缺陷时先写一条会失败的测试复现问题,再修代码,并单独提交。
- 测试失败时找根因,不改测试去迁就实现。 这次一条并发相关的测试失败,暴露的正是上面那个请求串行的问题。
- 不留兼容层。 用 codemod 一次性替换全部调用点,删除旧组件,避免新旧两条路径并存。
- 视觉回归要逐个解释差异。 每个像素差异都要找到原因,确认是随机端口、实时数据这类无关因素,才能判定为无变化。
- 记录有意的行为变化。 例如 URL 变了、错误改在别处显示,都要在文档里写明,不要只写"无变化"。
六、需要警惕的信号
出现以下任意一条,就该停下来拆分或重新设计:
- 组件里出现
xxx === 'a' && <整块布局/>的多路分支; - 一个组件里有超过 5 个 useState,或者同时持有多个弹窗的开关状态;
- 布尔 props 同时影响接口路径、可见功能和请求字段;
- 一个组件用
kind或type字段区分多种实体,并伴随类型擦除; - 用
useEffect让一份 state 跟着另一份 state 同步; - 同一段选项映射或格式化代码在两处以上复制;
- 前端判断规则和后端各写一份,但没有测试把两者绑在一起;
- 新增懒加载以后,首屏请求的时间线变成了串行。