源码教程:等待条件与 JavaScript 执行
异步页面不能只靠固定 sleep。等待命令分别判断元素、URL、网络、DOM 或数量条件;条件成立说明观测到了某种状态,不自动证明业务完成。
1. 原生等待与自定义等待
| 命令 | 实现思路与适用范围 |
|---|---|
wait | 按 seconds 等待;没有业务条件,适合明确要求的短暂间隔 |
wait_for_element | 在指定 frame 内构造 Locator 并等待,适合异步出现的控件 |
wait_for_text | 使用文本条件等待页面内容 |
wait_for_url | 调用页面 URL 等待逻辑,适合确定的跳转 |
wait_for_load | 将 state 映射到加载状态;加载完成不代表所有业务异步请求结束 |
wait_for_function | 在页面反复求值 expression,直到条件成立 |
wait_for_idle | 结合 DOM 变化和在途请求,检查连续 quietMs 的安静窗口,可加 selector/text 条件 |
wait_for_stable | 轮询所选区域的文本、表单等状态,检查连续 quietMs 不再变化 |
wait_for_count | 反复 count,检查 min、max、equals 约束;没有数量条件时采用默认下限 |
timeoutSeconds 与 quietMs 单位不同。错误时保留实际状态和耗时比只返回“失败”更有帮助。长期轮询的网站可能永远不满足 idle;这时应等待业务文字、具体元素数量或某个数据区域稳定,而不是无限增加超时。
{"id":"1001","method":"wait_for_count","params":{"selector":"#results tbody tr","min":1,"timeoutSeconds":10}}
{"id":"1001","method":"wait_for_stable","params":{"selector":"#results","quietMs":500,"timeoutSeconds":10}}
列表稳定不代表它已经从旧结果刷新到新结果。先观察加载标记、请求完成或查询条件对应的结果标识,再等待稳定,能避免读取上一次的数据。
稳定等待的循环如何写
waitForStable 使用 contentProbe 获取内容指纹,指纹改变就重置稳定起点。下面省略响应构造,保留源码的判断顺序:
String last = null;
long stableSince = startedAt;
while (true) {
long now = System.currentTimeMillis();
Kv probe = contentProbe(inst, selector);
if (probe == null) {
return RespBodyVo.fail("读不到内容指纹");
}
String fingerprint = probe.getStr("fingerprint");
if (!Objects.equals(fingerprint, last)) {
last = fingerprint;
stableSince = now;
}
if (now - stableSince >= quiet) {
return RespBodyVo.ok();
}
if (now >= deadline) {
return RespBodyVo.fail("内容未在限定时间内稳定");
}
inst.page.waitForTimeout(150);
}
服务默认 quiet 为 800 毫秒;第一次采样也算一次变化。成功返回 stable、waitedMs、changes、fingerprint 等信息。这个循环并不检测“业务数据正确”,只检测被采样内容持续不变。
2. execute_js 的执行链
CommandTable 要求 body 或 bodyFile 至少存在一个。PlaywrightService.executeJs 选择目标 Frame,读取脚本,应用 vars,再交给 Frame.evaluate。后者会等待 Promise 结果,所以服务返回 awaited:true,无需为了同步结果使用阻塞 XHR。
{"id":"1001","method":"execute_js","params":{"body":"() => ({title: document.title, count: document.querySelectorAll('article').length})","retryOnSpurious":true}}
bodyFile 只能从配置允许的脚本目录读取;不能把任意服务器路径暴露成脚本执行入口。vars 用于替换模板变量:applyVars 将值 JSON 编码,再替换 {{key}} 或双引号包裹的 "{{key}}"。模板变量应占据完整的 JavaScript 值位置,不用于拼接标识符、属性名或半截字符串。
{"id":"1001","method":"execute_js","params":{"body":"() => document.querySelector({{selector}})?.textContent","vars":{"selector":"#results"},"retryOnSpurious":true}}
例如 selector 值编码后成为合法字符串字面量,避免调用方手工处理引号。normalizeScript 会把包含 return、且不是函数形式的片段包装成箭头函数;普通表达式保持原样。
返回的核心字段是 result,按需附 varsApplied、frame 信息。脚本错误会返回错误分类、预览和长度;包含凭据的脚本可能进入错误日志,因此脚本参数同样需要控制敏感内容。
3. 自动重试边界
evaluate 可能执行读取,也可能点击、提交或写入。服务不能仅凭命令名推断脚本安全,所以默认不自动重试。只有确认脚本没有副作用时才显式传 retryOnSpurious:true。导航、对象释放与脚本语法错误也必须区别处理。
Windows 命令行报 Unexpected end of input 时,检查脚本是否在传输中被截断,优先将参数写 UTF-8 JSON 文件,或使用 bodyFile;不要反复修改一段本来正确的 JavaScript。
4. 本地验证
fixture 在延时后增加一行表格,再在稍后修改单元格。分别验证 count、stable 和 function 的触发时机;用持续网络轮询验证 idle 能超时而不是误判成功。脚本测试覆盖字符串、对象、Promise、语法错误和只读脚本的有限重试。等待失败后先读取当前状态,不自动再次点击“查询”。
注册参数与 Java 入口
以下按 CommandTable 实际读取参数整理。* 表示注册层使用必填读取器;其余字段省略后由服务决定默认行为。带条件的入口仍需满足正文说明,例如上传文件来源、元素定位二选一。外层 id 不重复列出。
| 命令 | params 字段 | Java 入口 |
|---|---|---|
wait | seconds* | waitSeconds |
wait_for_element | selector*、timeoutSeconds、frame | waitForElement |
wait_for_text | text*、timeoutSeconds | waitForText |
wait_for_url | url*、timeoutSeconds | waitForUrl |
wait_for_load | state、timeoutSeconds | waitForLoad |
wait_for_function | expression*、timeoutSeconds | waitForFunction |
wait_for_idle | quietMs、timeoutSeconds、selector、text | waitForIdle |
wait_for_stable | selector、quietMs、timeoutSeconds | waitForStable |
wait_for_count | selector*、min、max、equals、timeoutSeconds | waitForCount |
execute_js | body、bodyFile、vars、frame、retryOnSpurious | executeJs |
