一、案发现场:收银台突然进入"请勿触摸"模式
上周监控飘红,用户说得特别具体:点完"确认支付",加载圈转得挺欢,但想滚一下看看小字、想点返回、甚至想关窗——全都不好使。不是卡死,是那种礼貌的拒绝:像在玻璃栈道外面摸了钢化玻璃,手感冰凉,没用。
我第一反应其实是网络,第二反应是是不是又把遮罩层盖坏了。结果真凶藏得很土:SDK 里一段"怕重复扣费"的小心思,给 body 加了个类名。
// 本意:别让用户手抖连点
function onPaying() {
document.body.classList.add('noclick');
}配上这一句,堪称寸草不生:
.noclick {
pointer-events: none;
}这段代码写出来的时候大概很自信:锁住世界,保住订单。用户心里想的大概是:我还健在吗?
二、这不是玄学:pointer-events 真的会"株连九族"
`pointer-events` 是个很会继承的属性。你贴在 body 上,等于给整棵 DOM 树发了张集体放假条——子元素默认跟着喝西北风。按钮还在,滚动条也在,但事件流在边缘就被晾着了。你点的不是"关闭",是寂寞。
于是出现一个特别反直觉的现象:动画还能转,因为那是渲染;你想互动,因为事件进不来。排查时如果只听"页面卡",很容易在性能面板里绕远路。我这回学乖了:先看 Elements 面板里 body 头顶糊了啥 class,再看有没有全屏 pointer 拦截。
这里 AI 替我干了件小事:我把"点击支付后除了 loading 啥都不能点"口述给它,它在一分钟内列出了五六条常见路径(body 锁、透明遮罩、错误捕获阶段监听、甚至 focus trap)。不是每条都对,但它把"从最啰嗦到最离谱"排好了序,我省下的是翻三百页论坛帖的心力。
原理上可以粗暴理解成:大模型做的是模式补全——在你的只言片语里找和训练语料最像的故障家谱。它不知道你们产品经理上周改了啥,但在"症状 → 常见写法"这件事上,它比我的短期记忆宽。记住:它只是候选生成器,验货真相比对还得你来。
三、救人一命:从"封城"改"封门"
真正的修复不需要什么魔法,需要的是克制。
1. 给按钮发身份证
每个会触发扣款的核心按钮,挂稳定 `id` 或 `data-*`,别再默认全世界都要为你陪葬。
2. 把"嫌疑人"告诉 SDK
别让 SDK 默默假设"我要锁 body"。显式传 `lockButtonId`,谁点的就只摁住谁。
3. 局部锁的朴素写法
function acquireLock(option) {
const target = getTargetElement(option.lockButtonId);
if (target && target !== document.body) {
target.classList.add('btn-disabled');
} else {
// 真走到这一步,说明信息丢干净了——再考虑遮罩+可解释的兜底
applyGlobalOverlay();
}
}AI 在这步又摊了一层薄饼:它按我们团队的代码风格吐了一版 diff 形状,顺手提醒"别忘了 focus、aria-disabled、以及失败路径解锁"。我都采纳了吗?没有。但它让我少和 linter 吵两轮,且把"容易忘的边角"提前摆在桌面上——这就是质量提升里最不性感但最值钱的部分:检查清单变长了,背锅半径变短了。
顺便:人和模型的分工,可以很土但很准——模型扩宽度,人保深度。宽度是"还能是什么";深度是"在我们系统里到底是什么"。中间那一道闸,叫人在回路(human-in-the-loop):模型的输出只是建议分布,你的断点才是上线许可。
四、上线之后:世界又能摸了
改完之后,收银台终于回到正常人类的触感:支付中等归等,但至少还能滚条款、还能在网烂时体面退出。业务侧的确定性也在——该锁的按钮照样锁死,只是不再把整个浏览器当嫌疑犯捆起来。
结语
前端写防御代码,心里可以慌,手要稳。"最小影响原则"听起来像 PPT,翻译成人话就是:别用消防栓浇一杯拿铁。AI 能让你少熬两夜、少打两行重复废话,但它替不了你在用户手机里点那一下的良心。
安全要有,围城不必。
本文属于 AI 实用主义流派 的第 6 篇肉身实战。
发表评论
分享你的想法和反馈