Chrome DevTools Protocol
2017-08-24 / uncle.wang
Chrome 的 F12 想必大家已经再熟悉不过了,如果你是一名前端er,那玩意简直就像咱们的饭碗一样。。
这是 Chrome 自带的调试工具,平常 debug 页面全靠它,然而它却是通过一个叫 Chrome DevTools Protocol 的协议来和目标页面通讯的。
chrome v 60 + 可以用下面参数暴露出协议。
chrome.exe --remote-debugging-port=9222
Chrome DevTools Protocol 是基于 WebScoket 协议的,当使用上面代码启动 chrome 后,我们可以在另一个页面输入 localhost:9222 来查看所有可以被 inspect 的页面。
上面的 google 和 tmall 正对应着当前打开的 2 个页签。
点击任意一个,进入调试页面。
可以看出 chrome 的调试器本质也是一个 web 页面而已。这是我们打开调试器的调试器。
这是一个奇妙的场景,上下 2 个调试器,上面那个是用来调试目标页面的,下面那个是调试器的调试器。点击下面的调试器,切换到 NetWork 页签, F5 刷新一下,会发现一个 WebSocket 连接。
这个就是 devTool 用于连接调试页面的连接了,这个 websocket 连接遵循 Chrome DevTools Protocol 协议
看一下它们之间到底发送了什么东东。。
{"id":1,"method":"Network.enable","params":{"maxTotalBufferSize":10000000,"maxResourceBufferSize":5000000}}
{"id":2,"method":"Page.enable"}
... ...
{"id":36,"method":"Overlay.highlightNode","params":{"highlightConfig":{"showInfo":true,"showRulers":false,"showExtensionLines":false,"contentColor":{"r":111,"g":168,"b":220,"a":0.66},"paddingColor":{"r":147,"g":196,"b":125,"a":0.55},"borderColor":{"r":255,"g":229,"b":153,"a":0.66},"marginColor":{"r":246,"g":178,"b":107,"a":0.66},"eventTargetColor":{"r":255,"g":196,"b":196,"a":0.66},"shapeColor":{"r":96,"g":82,"b":177,"a":0.8},"shapeMarginColor":{"r":96,"g":82,"b":127,"a":0.6},"displayAsMaterial":true,"cssGridColor":{"r":75,"g":0,"b":130}},"nodeId":4}}
可以看出每条信息的格式是有一个递增的 id 值,然后有 method 和 params 参数。这些消息指挥者被调试页面做出各种各样的动作,比如 hover 到 node 上让该节点高亮要使用 "Overlay.highlightNode"。其他具体的命令用法可以去官网查询。
也就是说,任何一个实现了 Chrome DevTools Protocol 的程序都可以用来调试页面,chrome 这个协议等于是开放了用程序控制页面动作的接口。
下面上一个小例:
const initlize = [
{"id":1,"method":"Network.enable","params":{"maxTotalBufferSize":10000000,"maxResourceBufferSize":5000000}},
{"id":3,"method":"Page.getResourceTree"},
{"id":4,"method":"Profiler.enable"},
{"id":5,"method":"Runtime.enable"},
{"id":6,"method":"Debugger.enable"},
{"id":7,"method":"Debugger.setPauseOnExceptions","params":{"state":"none"}},
{"id":8,"method":"Debugger.setAsyncCallStackDepth","params":{"maxDepth":32}},
{"id":9,"method":"DOM.enable"} ,
{"id":10,"method":"CSS.enable"} ,
{"id":11,"method":"Overlay.enable"},
{"id":12,"method":"Overlay.setShowViewportSizeOnResize","params":{"show":true}},
{"id":13,"method":"Log.enable"},
{"id":14,"method":"Log.startViolationsReport","params":{"config":[{"name":"longTask","threshold":200},{"name":"longLayout","threshold":30},{"name":"blockedEvent","threshold":100},{"name":"blockedParser","threshold":-1},{"name":"handler","threshold":150},{"name":"recurringHandler","threshold":50},{"name":"discouragedAPIUse","threshold":-1}]}},
{"id":15,"method":"ServiceWorker.enable"},
{"id":16,"method":"Inspector.enable"},
{"id":17,"method":"Target.setAutoAttach","params":{"autoAttach":true,"waitForDebuggerOnStart":true}},
{"id":18,"method":"Target.setAttachToFrames","params":{"value":true}},
{"id":19,"method":"Target.setDiscoverTargets","params":{"discover":true}},
{"id":20,"method":"Target.setRemoteLocations","params":{"locations":[{"host":"localhost","port":9229}]}},
{"id":21,"method":"Page.setAutoAttachToCreatedPages","params":{"autoAttach":false}}
];
var socket = new WebSocket('ws://localhost:9222/devtools/page/70025d54-d942-446d-9bf9-ffdd93b7896d');
socket.onmessage = function (e, a) { console.log(e, a) }
socket.onopen = function () {
for (let i in initlize) {
socket.send(JSON.stringify(initlize[i]))
}
}
socket.send(`{"id":22,"method":"Runtime.compileScript","params":{"expression":"alert(1)","sourceURL":"","persistScript":false,"executionContextId":1}}`);
socket.send(`{"id":23,"method":"Runtime.evaluate","params":{"expression":"alert(1)","objectGroup":"console","includeCommandLineAPI":true,"silent":false,"contextId":1,"returnByValue":false,"generatePreview":true,"userGesture":true,"awaitPromise":false}}`);
上面我们模拟了发送 alert(1) 到目标页面的过程。
用 puppeteer 控制 headless chrome
直接操作协议有点痛苦, chrome-remote-interface 封装了协议的大部分功能,但还是需要阅读协议内容,去看,去查。这还是有些痛苦啊。。。 puppeteer 提供了比较高级的 API,使用非常简单,推荐一波。
const puppeteer = require('puppeteer');
(async() => {
const browser = await puppeteer.launch({
headless: true
}); // default is true
const page = await browser.newPage();
await page.setRequestInterceptionEnabled(true);
page.on('request', interceptedRequest => {
console.log(interceptedRequest);
// if (interceptedRequest.url.endsWith('.png') || interceptedRequest.url.endsWith('.jpg'))
// interceptedRequest.abort();
// else
// interceptedRequest.continue();
});
await page.setViewport({
width: 1024,
height: 768
});
await page.goto('https://www.jd.com', {
waitUntil: 'networkidle'
});
const result = await page.evaluate(() => {
return Promise.resolve(document.title);
});
console.log(result);
await page.screenshot({
path: 'screenshot.png'
});
browser.close();
})();
感觉使用它可以完全替代 phantomjs 等脚本浏览器了。google 大厂官方出品,可靠性绝对靠谱。
调试 nodejs
node 也支持 Chrome DevTools Protocol ,在 node 7+ 的版本,输入 node --inspect-brk app.js 即可开启调试。
node --inspect-brk app.js
然后打开 chrome 在地址栏输入: chrome://inspect