这一版是一次前端专项:把散乱的视觉样式收敛到单一真源,同时修掉一个源码完全正确、只有构建压缩之后才出错的隐蔽缺陷。

后者比前者有意思得多——它是这轮排查里最值得记下来的东西。

本文要点

  • 过滤器「默认能加载、点了就清空」的真凶不在前端逻辑,也不在后端,而在构建压缩把变量绑错了
  • 源码是对的、单元测试也是绿的,只有真正跑生产产物才会暴露
  • 颜色 / 字体 / 图标 / 间距四类散乱样式收敛到设计 token,视觉零变化
  • 为这类 bug 加了静态门禁:禁止用拼接生成 data-* 属性

一个「源码对、产物错」的 bug

现象很普通:草稿审阅页和死信队列页,默认能加载出数据,点「全部 / 待处理」等过滤器标签,列表就清空了。两个页面,同一个症状。

按常规路径排查了一圈:后端 status=all 返回正常(实测 13 条);模板容器 ID 与 JS 引用一致;data-args 的 JSON 解析链路没问题;页面模块有 window 注册;没有 form 包裹导致误提交。全部正常。

转折点是一个不起眼的细节——用真实 dist 产物而不是源码去执行那段 tab 生成代码时,看到的片段是这样的:

.map(function(H){ return '...data-args=\'["' + s + '"]\'' + ... })

回调参数是 H,但 data-args 里的 s 没有跟着改——它指向了外层另一个同名变量。

于是整条链路就通了:var t = [...].map(...), s = '<button>全部</button>' 是顺序求值的,.map() 执行时外层 s 还是 undefined → 生成的 data-args 成了 ["undefined"] → 事件分发时 JSON.parse 解析出一个 undefined → 请求 /api/...?status=undefined → 后端按字面量 WHERE status='undefined' 查询 → 空列表。

为什么它能一路逃过审查

因为源码本身是完全正确的:变量声明、拼接顺序、参数传递都没问题。错的是压缩器把拼接重排成了模板字面量片段:

源码:'...data-args=\'["' + s + '"]\''

产物:'...data-args=\'["' + s + '"]\'' —— [" 和 "] 变成了模板字面量,+s+ 夹在中间

这意味着:读代码看不出问题;单元测试直接调用函数也会「通过」——因为它加载的是源码。只有真正跑生产产物,或者在真实构建产物上做断言,才会暴露。

教训:验前端 bug 必须验产物。源码正确不等于产物正确——尤其当产物要经过压缩、合并、转译时。

修法,以及顺带暴露的门禁问题

修法本身很简单:改用模板字符串插值(${},变量作用域绑定,压缩安全)。改完后产物是干净的 data-args='["${H}"]'。

但更重要的是补上门禁——这类错误只要有人再写一次字符串拼接就会复发。新增 scripts/check_data_args_concat.mjs:任何用 + 变量 + 拼接生成 data-* 属性值的写法都会让构建失败,已接进 verify:frontend 与 CI。回归测试也改成直接针对 minify 产物断言,而非源码。

门禁写完后我都做了一次反向验证:把 bug 原样注入回去,确认门禁和测试真的会红,再还原修复确认变绿。否则那只是一段看起来很严谨的空代码。

样式收敛:四类散乱值归位

顺便做了件一直想做的事——把界面上散落的样式值收敛到设计 token。原先的状态是多种来源混用:

现已全部折叠为 token(--brand-* / --gray-* / --font-mono / 新增 --icon-xs 到 --icon-3xl 七档刻度 / 启用 --space-*)。全部是保值映射,视觉零变化——改的只是「值的来源」,不是「值本身」。

这里有个教训值得记:第一版我顺手把中等亮度的装饰性文字色也映射到浅色 token,结果暗色主题下那些 token 是 12% 透明度,文字直接隐形了。发现后整体回滚重做,改成只折叠无歧义的部分(重复灰阶 + 强语义色),浅底和中间调保持字面值。

统一风格这件事,保值优先于彻底。宁可少统一几处,也不要为了「干净」引入可见性回归。

顺带修掉的几个交互问题

门禁

升级注意

无破坏性变更、无新增配置、无数据迁移。纯前端改动。若浏览器缓存了旧 bundle,升级后建议强制刷新一次。

延伸阅读