这一版是一次前端专项:把散乱的视觉样式收敛到单一真源,同时修掉一个源码完全正确、只有构建压缩之后才出错的隐蔽缺陷。
后者比前者有意思得多——它是这轮排查里最值得记下来的东西。
本文要点
- 过滤器「默认能加载、点了就清空」的真凶不在前端逻辑,也不在后端,而在构建压缩把变量绑错了
- 源码是对的、单元测试也是绿的,只有真正跑生产产物才会暴露
- 颜色 / 字体 / 图标 / 间距四类散乱样式收敛到设计 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。原先的状态是多种来源混用:
- 颜色:Tailwind 灰阶与 Bootstrap / GitHub Primer / Ant 的色值混用,同一语义存在多个近似但不同的值(灰阶同时有
#6b7280和#64748b),主色有 4 种蓝、3 种红; - 字体:等宽字体有 10+ 种手写栈(
'SF Mono'、ui-monospace、Monaco…); - 图标:字号 25+ 种散值(0.55rem 到 2rem),同档位抖动(0.7 / 0.72 / 0.78 并存);
- 间距:
--space-*token 定义了却零引用,全站仍用魔法值。
现已全部折叠为 token(--brand-* / --gray-* / --font-mono / 新增 --icon-xs 到 --icon-3xl 七档刻度 / 启用 --space-*)。全部是保值映射,视觉零变化——改的只是「值的来源」,不是「值本身」。
这里有个教训值得记:第一版我顺手把中等亮度的装饰性文字色也映射到浅色 token,结果暗色主题下那些 token 是 12% 透明度,文字直接隐形了。发现后整体回滚重做,改成只折叠无歧义的部分(重复灰阶 + 强语义色),浅底和中间调保持字面值。
统一风格这件事,保值优先于彻底。宁可少统一几处,也不要为了「干净」引入可见性回归。
顺带修掉的几个交互问题
- 弹窗背景可滚动:弹窗是全屏遮罩,但背后内容区仍能滚轮滚动。已补背景滚动锁——锁的是真正的滚动容器(
.content-area),而不是body(后者本来就不滚动,锁了没用)。 - 图片灯箱监听器泄漏:反复打开大图会重复绑定 Esc 监听;且直接访问节点没有空值保护。现补空值守卫 + 防重复绑定标志。
- 错误提示不变红:知识库页 4 处引用了一个只在某个局部作用域定义的
--danger,不是:root全局变量 → CSS 声明失效 → 错误文字继承默认色。改用规范的--brand-danger。 - 死信搜索框是死按钮:模板引用了
debouncedLoadDeadLettersPage,代码里根本没有这个函数。已补上。
门禁
- 前端测试全量通过:132 项(含新增 4 项针对构建产物的回归)。
- ESLint、bundle 冒烟、运行时冒烟、依赖一致性(0 告警)、ruff 全路径通过。
- 新增的门禁与测试都做过「注入故障 → 确认报错」验证,确保不是空跑。
升级注意
无破坏性变更、无新增配置、无数据迁移。纯前端改动。若浏览器缓存了旧 bundle,升级后建议强制刷新一次。
延伸阅读
- 完整变更日志(CHANGELOG)——本次全部变更条目
- GitHub Release v0.6.2
- 项目源码——GitHub