开发日志 2026-09-29:空态下的占位样式、失同步的计数列与重构中的迁移清单
前言
今天的主题词是「同源」:列表容器露灰底,是占位样式和实际内容不同源;过滤漏洞,是筛选口径和展示口径不同源;企业卡片「暂无职位」,是冗余计数列和真实数据不同源;投票按钮消失,是旧布局的功能和新布局的界面不同源。一天五单,全部指向同一句工程常识——展示给用户看的,和用来做判断的,必须是同一份数据、同一套口径。
一、空态下的占位样式:min-height + 灰底的经典陷阱
小程序「作品」列表出现一个怪现象:作品数不足一行时,卡片右侧和下方露出一大块灰色空底。排查发现,列表容器的样式写死了 min-height: 50vh 加灰色背景——本意大概是让页面在视觉上「撑得满」,但内容不足半屏时,容器被强行撑高,多出来的部分就以灰色空底的形式暴露出来。
修复只删了两行:去掉 min-height,灰底改白底。容器高度随内容收缩,空出的格子回归白底、不可见,问题消失。同构的几个组件逐一检查,同样的问题一并修掉。
这类问题的本质是占位样式在空态下会变成「假内容」:min-height、占位背景色、骨架屏,这些为「内容足够」的场景设计的样式,在内容不足时会反向暴露。顺带处理了同页面的卡片间距——移除内联评论模块后,一段为它保留的 30rpx 呼吸边距成了多余,加上 margin 折叠的叠加效应,上下间隙一宽一窄。删掉旧功能时,记得把它配套的样式配额一起收回。
二、过滤口径的两端对齐:模型层一处改,调用处零改动
内容社区平台有个常见需求:被运营标记为「不推荐」的作品,不应出现在推荐列表和最新列表里。这次在两侧分别落了刀:
- 后端:最新列表的 SQL 在 WHERE 条件里加一条「推荐状态 ≠ -1」。验证方式是直连数据库对账——改动前后列表总数从约 5.15 万降到 5.14 万,逐条核对被排除的记录,全部是标记为不推荐的,没有误伤。还要确认这条链路上没有列表缓存(只有刷浏览量的计数缓存),改动才能即时生效。
- 前端:小程序推荐列表的过滤放在模型层——接口返回后、回调给页面前统一剔除。这个模型被首页两处调用(列表加载 + 预加载),一处修改两处生效,页面代码零改动。
过程中还踩了一个经典的「改了没生效」:SQL 改完实测,被过滤的作品还在返回。排查发现本地服务是凌晨启动的旧进程,加载的还是旧 SQL——代码在磁盘上是新的,进程内存里是旧的。重启后用「锚点对照」验证:记住某条不推荐作品在列表中的位置,重启前后各取一次,确认旧代码返回它、新代码跳过它,再全量翻页核对。改数据层代码的验证,要构造一个能看出差异的具体锚点,而不是刷新页面扫一眼。
三、失同步的计数列:EXISTS 实时校验替代陈旧冗余
招聘模块的首页出现「暂无职位」的公司卡片——筛选逻辑明明排除了无职位的公司。根因是一个经典反模式:表里有一根冗余计数列记录公司在招职位数,筛选时靠它 > 0 判断;而卡片上实际展示的职位数却是按职位表实时子查询算的。计数列维护不全,两边口径脱节——一百多家企业里九成以上计数与实际不符,两个方向都有偏差,于是「计数 > 0 过了筛、实际 0 职位展示空卡片」,反向的「计数 0 被误杀、实际有职位」也在发生。
修复改了一处 SQL:筛选分支不再信计数列,改用 EXISTS 实时校验职位表,条件与展示职位数的子查询完全一致——筛选口径和展示口径从此同源,不会再分叉。直连库模拟改后 SQL 随机抽样二十轮,零空卡片,顺带把原来被误杀的企业也放行了。
冗余计数列(denormalized counter)本身不是错——高并发场景下它是必要的性能妥协。错的是写路径不维护它、读路径又依赖它。要么在职位增删改的事务里同步维护计数,要么读路径放弃它改用实时校验。最怕的是两头都不管:它既不准确,又还在被信任。全量校准的修复 SQL 已备好,但要先找到漏更新计数的写路径,否则校准完还会再漂移。
四、重构中的迁移清单:废弃 ≠ 迁移
活动作品详情页的投票按钮「以前有、现在没了」。定位过程颇有戏剧性:纸面上所有条件都成立——投票逻辑完整、按钮代码还在、活动类型匹配——但按钮就是不渲染。跑起来实测才发现,整个详情页已经重构为「左内容 + 右侧栏」新布局,带投票按钮的旧头部组件被 v-if="false" 整体废弃,而按钮从来没有迁移到新布局的作者信息卡里。
修复是把按钮加进新布局的作者卡,替换掉活动场景下无意义的「关注 / 进入主页」按钮,事件接到父页现成的投票方法——逻辑层一行没动,只补了界面入口。浏览器实测:按钮显示、投票、取消、返回活动页全链路通过。
这单的教训值得记进重构纪律:把一个组件设为不可见,等于删除了它的全部功能——用户不会区分「代码还在」和「按钮还在」。重构布局时需要一份功能迁移清单:旧布局上每一个可交互元素,要么在新布局有对应物,要么有明确的「确认废弃」决策。「它只是被隐藏了」是一种自欺,对用户而言与删除无异。
五、小程序 UI 自动化的多层兜底
移除列表内联评论模块后需要验证两件事:评论区消失、弹窗评论入口仍可用。自动化这条路的现实比文档曲折得多:
miniprogram-automator连不上——开发者工具的自动化端口没开;- 用 CLI 拉起端口(顺带发现一个卡死 97% CPU 的残留 CLI 进程);
- 连上了,但选择器穿不透自定义组件的嵌套层级;
- 换服务层
evaluate拿组件实例——也拿不到; - 最终用 computer-use 兜底:直接对模拟器截图、读无障碍树确认「评论」文本已消失,再模拟真实点击走「… → 评论」弹窗链路,验证通过。
心得是验证工具链要预设多层兜底,且每层的验证力度不同:结构断言(选择器、组件树)被挡住时,视觉断言(截图、无障碍树)和行为断言(真实点击)往往仍可达成。别在第一层工具失灵时就退化成纯人工点检——降级到下一层自动化,好过跳到手工。
结语
占位样式要为空态收窄,筛选与展示要共用一处口径,计数列要么维护要么别信,重构要有迁移清单,自动化要有兜底层级。五单工作收在同一条线上:系统里每一条被信任的信息,都要能回答「它和真相是否同源、由谁保证」。答案含糊的地方,就是下一个 bug 的坐标。