Debugger API(Chrome DevTools 级断点调试)
debugger 挂在逻辑层与面板层两个全局变量上(wadLogicLayer.debugger / wadPanelLayer.debugger),把宿主里的 V8 Inspector / XWeb Inspector 能力包装成一组 REST + Promise API;渲染层相关入口统一为跨层代理(renderLayer.screenshot 等),大内容直传统一为 http.upload。
宿主路由:
/v1/debugger/*、/v1/render/*、/v1/upload插件 SDK:debugger.*(面板层与逻辑层都可用) 核心承诺:断点暂停的是小程序逻辑层;插件自身的逻辑/面板代码照常执行。
本页你将学到
- 断点 / 动作断点 / 函数断点的三种下法;
- 暂停现场、局部变量、帧内求值;
- 网络断点与渲染层截图/DOM/eval;
- 已知限制与错误码。
它是什么
| 能力 | 说明 |
|---|---|
| 断点 | 按 URL 正则 / scriptId / 函数对象下断点;支持列号(压缩成单行的脚本)与条件表达式 |
| 预加载断点 | 源码还没加载就能下(Debugger.setBreakpointByUrl),脚本一解析立即生效 |
| 暂停现场 | 暂停原因、命中断点、调用栈(最多 40 帧)、每帧的 local/closure/global 作用域 objectId |
| 局部变量 | 读作用域全部变量;在栈帧上求值(表达式可直接赋值改局部变量/对象状态) |
| 步进 | stepOver / stepInto / stepOut / pause / resume |
| 源码 | 脚本列表、getScriptSource、全文搜索(searchInContent) |
| 网络断点 | URL 正则命中 → 暂停逻辑层,并记录命中的请求 |
| 函数改写 | 在暂停帧上替换逻辑层函数(闭包内 e.request、游戏 checkHit2 等) |
| 渲染层 | WebView 截图(Page.captureScreenshot)、全量 DOM(outerHTML + DOM 树)、任意 render eval |
| 小游戏 | 逻辑层 contexts 选上下文 + GameGlobal.canvas.toDataURL() 画布截图 |
| 逃生口 | debugger.cdp(method, params) 直接打任意 DevTools 协议方法 |
为什么断点不会卡住插件
插件面板是宿主 UI 里的独立 WebView,插件的逻辑段跑在面板/总线侧;逻辑层被 V8 暂停时,debugger.* 的调用、轮询、上传都继续工作。
快速上手
(async () => {
// 面板层(wadPanelLayer.debugger)或逻辑层(wadLogicLayer.debugger)都可以
await wadPanelLayer.debugger.enable();
// 在 WACloud.js 的 get_service_info 调用处下断点(源码未加载也可以)
const bp = await wadPanelLayer.debugger.break('WACloud\\.js', 0, { column: 340757 });
// 长轮询等命中
const r = await wadPanelLayer.debugger.paused(10000);
if (r.paused) {
const f = r.state.frames[0];
console.log('暂停于', f.url, f.line, '函数', f.fn);
// 读局部变量
const vars = await wadPanelLayer.debugger.scope(f.scopes[0].objectId);
console.log(vars.vars);
// 直接改局部变量/对象状态
await wadPanelLayer.debugger.eval(f.id, 'system = "WAD-MODIFIED"');
// 步过 / 继续
await wadPanelLayer.debugger.step('over');
await wadPanelLayer.debugger.resume();
}
})();生命周期
enable() / status()
enable() 幂等开启 Debugger + Runtime 域。切换小程序后宿主会自动检测并重新 enable(旧断点/脚本表清空)。
{"enabled":true,"paused":false,"scripts":1484,"breaks":2,"netBreaks":0,
"appId":"wx…","contexts":3}断点
break(urlRegex, line, opts?)
按 URL 正则下断点,源码未加载也生效。
| 参数 | 类型 | 说明 |
|---|---|---|
urlRegex | string | 正则字符串,如 "WACloud\\.js"、"usr/game\\.js" |
line | number | 0 基行号 |
opts.column | number | 0 基列号。压缩成单行的脚本必须用列定位 |
opts.condition | string | 条件表达式,为真才暂停 |
返回 {ok, id, locations:[{scriptId,lineNumber,columnNumber}]}。id 用于 unbreak。
(async () => {
await wadPanelLayer.debugger.break('usr/game\\.js', 191, {
column: 36920,
condition: 'this.hit===0||this.hit===3||this.hit===4||this.hit===5||this.hit===6'
});
})();breakScript(scriptId, line, opts?)
脚本已加载时按 scriptId 下断点(参数同上)。
breakFn(objectId, condition?)
函数调用断点。objectId 来自 evalGlobal(expr, false);命中时 arguments 可直接改。
(async () => {
const fn = await wadPanelLayer.debugger.evalGlobal('a.SaaSServiceMap.get(this).refreshTunnelInfo', false);
await wadPanelLayer.debugger.breakFn(fn.objectId);
// 命中后:arguments[0].serializedReqData = ...
})();unbreak(id) / clearBreaks() / breaks()
删除/清空/列出断点。clearBreaks 会同时 resume,避免小程序被遗留在暂停态。
动作断点(auto-resume,不卡顿)
给 break 传 action:命中后宿主立即在栈帧上执行该表达式,然后自动 resume——只暂停「一次求值往返」的几毫秒,小程序不会卡住,面板也不用轮询/手动 resume。这就是 Chrome「条件断点 / logpoint」的等价物,适合做热修复、无敌、埋点。
(async () => {
await wadPanelLayer.debugger.break('usr/game\\.js', 191, {
column: 36920,
condition: 'this.hit===0||this.hit===3||this.hit===4||this.hit===5||this.hit===6',
action: 'this.hit=2', // 命中→改状态→自动继续
maxHits: 0 // 0=不限次数
});
// 查看动作断点执行结果(宿主侧环形日志,最多 100 条)
const log = await wadPanelLayer.debugger.actionLog();
// {"items":[{"id":"…","action":"this.hit=2","result":{"ok":true,"value":2},"at":…}]}
})();action 的返回值会记录在 breaks() 的 info.last 与 actionLog() 里;求值异常不会中断小程序,只记录 result.ok=false。
暂停现场
watch(onPaused, intervalMs?) · 面板层
轮询 paused(wait);每次命中暂停,回调 onPaused(state)。返回停止函数。只存在于面板层(wadPanelLayer.debugger.watch),逻辑层没有。
const stop = wadPanelLayer.debugger.watch(function (state) {
wadPanelLayer.log('paused at', state.reason);
}, 500);
// stop();paused(waitMs?)
waitMs > 0 时长轮询(最长 20s),暂停/恢复事件发生立即返回。
{
"ok": true, "paused": true, "seq": 12,
"state": {
"paused": true, "reason": "other",
"hitBreakpoints": ["2:191:36920:usr/game\\.js"],
"frames": [{
"id": "…2.0", "fn": "value", "scriptId": "147", "line": 191,
"url": "https://usr/game.js",
"scopes": [
{"type": "local", "objectId": "…2.1"},
{"type": "closure", "objectId": "…2.2"},
{"type": "global", "objectId": "…2.5"}
]
}]
}
}scope(objectId) / props(objectId)
读作用域/对象的全部属性:
{"ok":true,"vars":[{"name":"this","type":"object","description":"…","objectId":"…"},
{"name":"e","type":"number","value":3}]}求值(读/写状态)
eval(frameId, expression)
在暂停栈帧上求值,returnByValue: true。表达式可以直接赋值:
(async () => {
await wadPanelLayer.debugger.eval(f.id, 'this.hit = 2'); // 改对象状态
await wadPanelLayer.debugger.eval(f.id, 'this.UI.score = 9999'); // 改分数
await wadPanelLayer.debugger.eval(f.id, 'this.UI.scoreText.setScore(9999)');// 同步 UI
await wadPanelLayer.debugger.eval(f.id, 'p = "WADMOD_" + Date.now()'); // 改局部变量
})();evalGlobal(expression, byValue?, contextId?)
逻辑层全局求值。
byValue=false时保留objectId,可交给breakFn/props。contextId用于小游戏(GameGlobal不在默认上下文)。先用contexts()找。
(async () => {
const cs = await wadPanelLayer.debugger.contexts();
// {"items":[{"id":1,"name":"MainContext"},{"id":2,"name":"WxaSub-…","isDefault":true}]}
await wadPanelLayer.debugger.evalGlobal('typeof GameGlobal', true, 2);
})();求值的坑
宿主会显式带 contextId(这个 V8 构建没有 default execution context)。不要使用 awaitPromise:true(该构建会 SIGSEGV),异步结果用轮询。
步进
| 方法 | CDP |
|---|---|
step('over') | Debugger.stepOver |
step('into') | Debugger.stepInto |
step('out') | Debugger.stepOut |
step('pause') | Debugger.pause |
resume() / step('resume') | Debugger.resume |
源码与搜索
scripts(filter?) / source(scriptId) / search(query, filter?, opts?)
| 方法 | 说明 |
|---|---|
scripts(filter?) | 已加载脚本列表(filter 为 URL 子串) |
source(scriptId) | Debugger.getScriptSource 原文 |
search(query, filter?, opts?) | 全文搜索(等价 DevTools Ctrl+Shift+F),逐脚本 Debugger.searchInContent |
{"ok":true,"items":[{"scriptId":"147","url":"https://usr/game.js","endLine":468}]}{"ok":true,"scanned":58,"count":5,
"items":[{"scriptId":"147","url":"https://usr/game.js","line":191,"column":5284,"text":"…"}]}opts.anonymous=true 时也搜匿名脚本(宿主注入的代码也在此列);opts.max 限制条数(默认 500)。
网络断点
(async () => {
await wadPanelLayer.debugger.netBreak('a1d794322-.*\\.wxgateway\\.com', 'POST');
// 命中后逻辑层会在下一条语句暂停;命中记录:
const hit = await wadPanelLayer.debugger.netHit();
// {"hit":{"method":"POST","url":"https://…/__wx__/call","at":1794…}}
await wadPanelLayer.debugger.netBreakClear();
})();网络断点由宿主的网络捕获管线驱动,无论请求走哪条捕获路径都会触发。
渲染层:截图 / DOM / eval(入口:跨层代理 renderLayer.*)
screenshot(appid?, opts?)
XWeb Page.captureScreenshot(页面内容,不是 App 截屏),返回 {ok, png_b64, target}。opts.upload 非空时宿主直接 POST 截图,返回 {ok, uploaded, target, pngBytes, upload}(无 png_b64),绕过插件总线 200KB 帧限制。逻辑层与面板层均可用;渲染层没有 renderLayer 自引用入口。
(async () => {
// 面板层
const s = await wadPanelLayer.renderLayer.screenshot(); // {ok, png_b64, target}
// 带 upload 时返回 {ok, uploaded, target, pngBytes, upload}
await wadPanelLayer.renderLayer.screenshot('', { upload: 'http://192.168.1.10:9000/shot' }); // 宿主直传
})();domSnapshot(appid?, opts?)
全量 DOM:document.documentElement.outerHTML +(可选,opts.tree:true)DOM.getDocument {depth:-1,pierce:true} 树。多页小程序用 opts.route 选页面(宿主 POST /v1/render/dom {route:"shares/transaction/pages/menu/index"})。
(async () => {
const d = await wadPanelLayer.renderLayer.domSnapshot('', { tree: true });
d.html.length; // 62 万字符
d.tree.root; // CDP DOM 树
})();renderLayer.eval(expr)
页面上下文求值(元素 rect、点击、滚动等)。逻辑层侧为 fire-and-forget(返回 null),面板层返回 Promise 拿结果。
expr 推荐写单个表达式(document.title、__wadTextEdit(true));多条语句请包成 IIFE: (function(){ var a=1; doWork(a); return a; })()。宿主对多语句会自动按函数体执行兜底(v107+), 但直接写 a(); b; 这类裸语句在旧版本会触发 SyntaxError —— 跟着推荐写法走最稳。
小游戏没有 WebView
用 debugger.evalGlobal('GameGlobal.canvas.toDataURL("image/png")') 拿画布 PNG(仍属渲染层,不是 Java 截屏)。
通用上传(http.upload)
宿主直传任意内容到你的服务器(不受总线帧限制;面板层享受宿主直传通道):
(async () => {
// 面板层:走宿主直传,无 200KB 限制
await wadPanelLayer.http.upload('http://host:9000/csv', csvText, { contentType: 'text/csv' });
// 或 REST:POST /v1/upload {"url":"…","body":"…","contentType":"text/csv"}
// 逻辑层:wadLogicLayer.http.upload 是 wx.request POST 包装(返回统一 {statusCode,header,data})
})();逃生口
cdp(method, params)
直接打任意 DevTools 协议方法:
(async () => {
await wadPanelLayer.debugger.cdp('Runtime.getProperties', { objectId, ownProperties: false });
await wadPanelLayer.debugger.cdp('Debugger.getPossibleBreakpoints', {
start: {scriptId:'147', lineNumber:191, columnNumber:0},
end: {scriptId:'147', lineNumber:191, columnNumber:20000}
});
})();完整实战
跳一跳:断点 + 改状态 → 无敌
(async () => {
await wadPanelLayer.debugger.enable();
// 落点判定后(this.hit 已赋值)的语句;只在失败值时暂停
// action = 命中后自动执行并 resume —— 不轮询、不卡顿
await wadPanelLayer.debugger.break('usr/game\\.js', 191, {
column: 36920,
condition: 'this.hit===0||this.hit===3||this.hit===4||this.hit===5||this.hit===6',
action: 'this.hit = 2'
});
// 需要观察命中次数/结果时:
const log = await wadPanelLayer.debugger.actionLog();
})();跳一跳:分数 9999 + 画布截图
(async () => {
// 在任意暂停帧
await wadPanelLayer.debugger.eval(f.id,
'this.UI.score=9999; this.UI.scoreText.setScore(9999); this.currentScore=9999;');
// 画布 PNG(渲染层)
const shot = await wadPanelLayer.debugger.evalGlobal(
'GameGlobal.canvas.toDataURL("image/png")', true, gameCtxId);
await wadPanelLayer.http.upload('http://host:9000/shot', shot.value, { contentType: 'text/plain' });
})();多店铺商品采集 → CSV
(async () => {
// 页面路由(菜单页)
const ROUTE = 'shares/transaction/pages/menu/index';
const stores = await wadPanelLayer.debugger.evalGlobal(`
(function(){var p=getCurrentPages()[getCurrentPages().length-1];
return JSON.stringify(p.$vm.markerList.slice(0,5).map(function(s){return {n:s.storeName,id:s.storeId}}))})()
`);
for (const s of JSON.parse(stores.value)) {
// 门店选择器 → 选店 → 下单(宿主会预取菜单)
await wadPanelLayer.debugger.evalGlobal(`
(function(){var p=getCurrentPages()[getCurrentPages().length-1];var vm=p.$vm;
vm.storeSelect(vm.markerList.find(function(x){return x.storeId==${s.id}}));
setTimeout(function(){vm.makeOrder()},250); return 'ok'})()
`);
await new Promise(r => setTimeout(r, 6000));
const dom = await wadPanelLayer.renderLayer.domSnapshot('', { tree: false }); // 全量 DOM
// …解析 dom.html,追加 CSV 行
}
await wadPanelLayer.http.upload('http://host:9000/csv', csvText, { contentType: 'text/csv' });
})();限制与坑
| 限制 | 说明 |
|---|---|
| 压缩脚本 | 单行 500KB 的脚本必须用 column 定位;line 恒为 0/5 |
| 无默认上下文 | 必须带 contextId;宿主已自动处理,cdp 逃生口需自己带 |
awaitPromise | 该 V8 构建会 SIGSEGV,禁止使用;异步用轮询 |
| 总线帧 200KB | 面板 RPC 单帧上限;大图/大 CSV 用 http.upload 或宿主直传 |
| 小游戏 | 没有 XWeb target;截图走 GameGlobal.canvas.toDataURL |
| 多页 | render/dom 支持 route 选页面;render/eval 默认主页面 |
| 断点跨小程序 | 切小程序后 isolate 重建,宿主自动清空断点并重新 enable |
| 暂停态 | 逻辑层暂停时 evalGlobal 可能排队;优先用 eval(frameId, …) |
错误码
| code | 含义 |
|---|---|
E_ENABLE | Debugger.enable 失败(逻辑层未就绪) |
E_DEBUGGER | 断点/求值/步进失败(含 CDP 超时) |
E_SHOT | 没有渲染层 target / captureScreenshot 失败 |
E_DOM | DOM 获取失败 |
E_UPLOAD | 宿主上传失败(URL 非法/网络错误) |
E_TARGETS | 渲染 target 枚举失败 |
所有 Promise 拒绝的 Error 都带 .code,可直接分支处理。