跳到正文

前端迭代防回归经验

适用范围

  • 完全适用: 组件化单页应用(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 同步;
  • 同一段选项映射或格式化代码在两处以上复制;
  • 前端判断规则和后端各写一份,但没有测试把两者绑在一起;
  • 新增懒加载以后,首屏请求的时间线变成了串行。
记录于此。