CDP 自动化里「点了没反应」的六种真因:一周排障实录
用 CDP(Chrome DevTools Protocol)驱动真实页面做自动化,最常见的故障现象是同一句话:「我点了,但页面没反应」。这种时候人的第一反应通常是坐标算歪了,或者撞风控了。这一周在同一批站点上连续排了六次这类问题,结论是:点击本身几乎从来不是原因,原因都在点击之外。把六种成因和判定顺序记下来,下次能省掉半天。
成因一:坐标算对了,但它已经在视口外面
getBoundingClientRect() 返回的是相对视口的坐标,元素在文档里靠下时,中心点的 y 完全可能大于 innerHeight。这时 Input.dispatchMouseEvent 打过去什么也不会发生——不报错、不滚动、不冒泡,看起来就像「按钮失灵」。
const r = btn.getBoundingClientRect();
const x = Math.round(r.x + r.width / 2), y = Math.round(r.y + r.height / 2);
// 关键不是坐标,是这个空数组
document.elementsFromPoint(x, y) // -> [] 说明这点上根本没有可命中的元素
innerHeight // -> 554
y // -> 591 掉出视口 37 像素实测那次是某个站点的提交按钮,视口只有 554 高,按钮中心在 591。修法只有一行:btn.scrollIntoView({block: 'center'}),然后重新量一次 rect 再点。顺带一个坑:不同标签页的视口高度可以不一样(同一次会话里见过 593 和 504 两种),所以「这个坐标在别的页面上好用」不构成理由。
成因二:按钮的 onclick 是后来才挂上去的
有些表单的提交按钮初始状态是「死」的:DOM 上有元素、有样式、能点中,但事件处理器还没绑。典型写法是等某个前置条件满足后才 btn.onclick = 'submitForm(0)' 并摘掉一个 -no 后缀的类名。前置条件往往是「请同意协议」那个勾。
// 点之前先看有没有处理器,别急着怀疑点击
btn.getAttribute('onclick') // -> null 点了当然没反应
btn.className // -> "publish-btn-no"
// 先真实点到协议勾,再回头看这两个值,才轮到点提交判定成本几乎为零:getAttribute('onclick') 和 className 各读一次,三秒就能确认是不是这种情况。
成因三:组件只认某一种事件类型
老式 jQuery 插件常见。标签选择器(tagator)的内部状态 selected_index 只由 mouseover 和方向键设置,click 处理器读的是这个值。于是 $('.tagator_option').trigger('click') 不会报错,只会加错标签——实测莫名其妙加上了列表第一项。正确顺序是先过滤到只剩一项,再补 mouseover,最后才 click。
// 只 click 会读到过期的 selected_index
$opt.trigger('mouseover') // 这一步才真正设置 selected_index
$opt.trigger('click')
// 另一类:有的复选框只认 jQuery 的 trigger,真实鼠标事件和原生 dispatch 都不生效,
// 而且连点两下会翻回未勾选,所以必须回读 className 确认结果这一类的通用教训:点完要回读状态。「没有报错」不等于「生效了」。
成因四:后台标签页把过渡动画卡在了第一帧
Vue 的下拉浮层用 transition 展开,元素会依次经过 *-enter-from(此时 height 为 0)。如果这个标签页在后台,浏览器会推迟动画帧,浮层就永久停在 height 0。此时选项在 DOM 里存在、可查询,但命中测试全打空,怎么点都点不中。
三条出路:把标签页切到前台;或者绕开 UI 直接打接口(多数时候这才是正解);或者手动把 enter 相关类名摘掉再点。别在 UI 上跟它耗。
成因五:目标被一个看不见的模态框压住
桌面端 Electron 客户端会弹「发现新版本」,遮罩层把侧边栏压成 offsetWidth = 0,表现是「资源管理器里没看到那个文件」。实际是整条侧边栏不可见。
排查方法是沿着 elementsFromPoint 返回的栈一层层看是谁盖在上面;临时验证可以 pointerEvents = 'none' 把可疑遮罩禁掉再点。处理上要先关掉这类弹窗再继续,而且只点「稍后」——自动升级会让版本变化,整套类名选择器全得重新对。
成因六:浏览器进程本身状态坏了
最难查的一种,因为前五条的排查动作全都「正常」。有一次定时任务报「等待跳转超时」,截图里账号密码都填进去了,页面既不跳转也不报错,也不出验证码。真正的原因是自动化复用了一个人工开着的、会话已经过期的浏览器实例:那个登录页的内部状态早就坏了,提交函数在坏状态上直接 return。
同一个 profile、同一份凭据,换一个新起的浏览器进程一次就过。所以给无人值守任务的建议是:自己拉起、自己回收,绝不 attach 到已经存在的调试端口上。复用别人的浏览器省下的那一秒启动时间,会用几小时的排障还回去。
一套判定顺序
把上面的经验压成固定流程,遇到「点了没反应」按顺序走,不要跳步:
1. 量坐标:scrollIntoView 后重新取 rect,elementsFromPoint 必须非空
2. 看处理器:onclick / addEventListener 是否已挂,className 是否处于禁用态
3. 装探针:hook fetch 与 XMLHttpRequest,把请求塞进 window.__net
4. 点一次,读 __net —— 这一步把问题劈成两半
有请求 -> 是接口/参数/登录态问题,去 replay 响应
无请求 -> 是事件没落到处理器,回到 2/5/6
5. 换事件类型再试一次(mouseover / jQuery trigger / 键盘回车)
6. 都不成立才怀疑环境:后台标签页动画、遮罩层、浏览器进程状态第 4 步是分水岭,价值最大:「有没有真发出业务请求」是一个二值事实,而「感觉没点上」是主观判断。有了这一刀,就不会再去猜坐标、猜风控、猜浏览器版本。上面六种成因里有四种会在第 4 步直接暴露,剩下的两种(事件类型、进程状态)也能靠它排除掉一半。
小结
「点了没反应」从来不是一个症状,而是六个不同病名的共同表象:坐标出屏、处理器未挂载、事件类型不对、动画卡在后台标签页、遮罩层压顶、浏览器进程状态坏。它们的修法互不通用,但排查动作完全固定。把「先证明请求发出去了」变成本能,这类问题就从玄学变成了十分钟的体力活。
- 点赞
- 收藏
- 关注作者
评论(0)