用 Chrome DevTools Protocol 驱动浏览器做重复操作:五个真实踩坑
为什么要用 CDP
日常做技术平台的任务时,很多操作是"点固定位置、填固定内容"的机械动作。Playwright/Selenium 能覆盖大部分场景,但当目标页面已有登录态、或者需要复用用户自己的浏览器配置时,直接连 Chrome DevTools Protocol(CDP)往往更省事:启动一个带 --remote-debugging-port 的 Chrome,用 WebSocket 发 Runtime.evaluate 和 Input.dispatchMouseEvent 就能操作页面。
下面是我这次实际操作中遇到的五个问题,每个都附了现象和修法。
坑一:JS 的 .click() 对 Vue 组件无效
现象:document.querySelector('.btn').click() 调用后页面毫无反应,但手动点一下就好。
原因:很多 Vue 组件把事件挂在自定义包装元素上,或者依赖 mousedown/mouseup 的完整序列。单纯派发 click 事件不满足框架的判定条件。
修法:改用真实鼠标事件,坐标系从 getBoundingClientRect() 推算中心点:
const r = el.getBoundingClientRect();
const x = Math.round(r.x + r.width / 2);
const y = Math.round(r.y + r.height / 2);
await send('Input.dispatchMouseEvent', {type:'mouseMoved', x, y, button:'none', buttons:0});
await send('Input.dispatchMouseEvent', {type:'mousePressed', x, y, button:'left', buttons:1, clickCount:1});
await send('Input.dispatchMouseEvent', {type:'mouseReleased', x, y, button:'left', buttons:0, clickCount:1});
注意 mouseMoved 不能省,有的组件靠 hover 才显示可点区域。
坑二:后台标签页被节流,定时器几乎不跑
现象:把标签页切到后台后,页面里上报心跳的请求一次都没发出去,依赖"停留时长"的任务判定永远不通过。
原因:Chrome 会对不可见标签页做 timer throttling。
修法分两层:一是启浏览器时加参数;二是在 CDP 里把目标标签页激活,让它真在前台:
chrome --remote-debugging-port=9222 --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-renderer-backgrounding
await send('Target.activateTarget', {targetId});
如果目标页面要跑一段时间,别用 Page.navigate 反复跳,会让计时重置。
坑三:跨域 iframe 拿不到内容
现象:主页面里嵌了别的域名的 iframe,直接读 iframe.contentDocument 抛跨域异常。
原因:同源策略。这不是 bug,浏览器就该拦住。
修法:不要试图突破同源限制,改用 CDP 的浏览级会话直接找到那个 frame 的独立 target,或者用 Page.getFrameTree 配 Page.createIsolatedWorld。更简单的路是:在浏览器级 WebSocket 上 Target.createTarget 直接开一个新标签页访问目标 URL,绕开 iframe 嵌套。
const ver = await (await fetch('http://127.0.0.1:9222/json/version')).json();
const ws = new WebSocket(ver.webSocketDebuggerUrl); // 浏览器级,不是页面级
await send('Target.createTarget', {url: targetUrl, newWindow: false});
坑四:直接 fetch 接口被 CSRF 拦掉
现象:在页面上下文里 fetch('/api/xxx') 返回 403 no request_csrf found,但页面自己发的同一个请求是 200。
原因:前端封装层会给每个请求自动补一个 CSRF 头,而这个 token 来自某个 cookie 或全局变量,不一定是 document.cookie 里那几个常见的名字。
修法:先找 token 来源,再显式带上。我这次是从 cookie 列表里找到的:
const csrf = decodeURIComponent(
(document.cookie.split('; ').find(c => c.startsWith('csrf-eco=')) || '').split('=')[1] || ''
);
await fetch(url, {method:'POST', credentials:'include',
headers:{'Content-Type':'application/json', csrf}, body});
如果找不到,替代方案是开 Network.enable,让页面自己发请求,再用 Network.getResponseBody 抓响应体——完全不碰 CSRF。
坑五:读接口判断任务状态不可靠
现象:任务列表接口返回的 is_finish 是 0,但积分其实已经到账了。
原因:结算通常是异步的。行为发生和积分入账之间隔了几十秒到几分钟,任务状态字段和余额字段更新并不同步。
修法:以"余额流水"为唯一可信源,任务状态只作参考。查询余额和流水,比对时间戳:
// 余额
GET /growth/bonus-beans -> {"beans": 600}
// 流水(含时间和余额快照)
GET /growth/bonus-flow -> {"count":N, "BonusFlowList":[{detail, beans, commit_time, balance}]}
写自动化时,判定条件应该是"流水里出现了这条记录的 commit_time 在今天",而不是"任务状态变成已完成"。
小结
CDP 好用在于它操作的是真实浏览器,登录态、扩展、代理都不用重新配置;难用在它只给你原语,所有"业务语义"要自己补。上面五条的共同点是:先确认现象是框架层、浏览器层还是服务端造成的,再决定改哪一层。盲目加 sleep 或者反复重试,通常只是把问题推迟。
如果要做成长期可用的脚本,建议把"找元素 → 点 → 验证结果"封装成一次原子操作,每步都留截图和日志,出问题时能直接定位是哪一层的假设错了。
- 点赞
- 收藏
- 关注作者
评论(0)