blog icon indicating copy to clipboard operation
blog copied to clipboard

Chrome DevTools Protocol 协议详解

Open Pines-Cheng opened this issue 5 years ago • 2 comments

默认是 http://localhost:9222/devtools/inspector.html ,带模拟器;http://localhost:9222/devtools/devtools_app.html 不带模拟器。

DevTools 本质上可以看成是一个前端小应用,代码在这里: ChromeDevTools/devtools-frontend,当然,你也可以在 Chrome 浏览器直接打开:devtools://devtools/bundled/inspector.html 查看运行效果。

image

DevTools 是通过 Chrome 远程调试协议(Remote Debugger Protocal) 来和后端进行交互和调试的,

使用 DevTools 作为 protocol client

Developer Tools front-end 可以附加到远程运行的 Chrome 实例以进行调试。 为了让这个方案起作用,你应该使用 remote-debugging-port 命令行开关启动你的 host Chrome 实例:

Windows :chrome.exe --remote-debugging-port=9222
Mac : /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222

如果碰到证书问题,添加:--ignore-certificate-errors

然后你可以启动一个独立的 client Chrome 实例,使用一个不同的用户配置文件:

chrome.exe --user-data-dir=<some directory>

在这个场景中,您可以用自己的实现替换 Developer Tools 前端。你的应用程序可以通过请求: google http://localhost:9222 和获取一个 JSON 对象来发现可用的页面,这个 JSON 对象包含可检查页面的信息和 WebSocket 地址,你可以使用这些信息来开始检测它们。

GET /json or /json/list 获取可用的 websocket targets:

[ {
  "description": "",
  "devtoolsFrontendUrl": "/devtools/inspector.html?ws=localhost:9222/devtools/page/DAB7FB6187B554E10B0BD18821265734",
  "id": "DAB7FB6187B554E10B0BD18821265734",
  "title": "Yahoo",
  "type": "page",
  "url": "https://www.yahoo.com/",
  "webSocketDebuggerUrl": "ws://localhost:9222/devtools/page/DAB7FB6187B554E10B0BD18821265734"
} ]

DevTools protocol 通过 Chrome extension

参考:chrome.debugger extension API

Chrome Debugging Protocol

简单来说,远程调试协议就是利用 WebSocket 建立连接 DevTools 和浏览器内核的快速数据通道。那么我们也可以自己打开这个 WebSocket,遵从它的协议来发送消息。

在前面 inspector.html 和 Chrome Host 之间通过 WebSocket 建立连接后,从整个调试过程中的 Websocket 通讯可以看出,这个接口里面有两种通讯模式:Cmmand 和 Event。

  • Command:包含 request/response ,就如同一个异步调用,通过请求的信息,获取相应的返回结果。这样的通讯必然有一个message id,否则两方都无法正确的判断请求和返回的匹配状况。
request: {"id":1,"method":"Page.canScreencast"}
response: {"id":1,"result":{"result":false}}
  • Event:类似于 notification ,和第一种不同,这种模式用于由一方单方面的通知另一方某个信息。和“事件”的概念类似。
{"method":"Network.loadingFinished","params:{"requestId":"14307.143","timestamp":1424097364.31611,"encodedDataLength":0}}

远程调试协议把操作划分为不同的域 domain ,比如

  • DOM
  • Debugger
  • Network
  • Console
  • Timeline

可以理解为 DevTools 中的不同功能模块。每个域(domain)定义了它所支持的 command 和它所产生的 event(就是上面讲的两种通讯方式)。每个 command 包含 request 和 response 两部分,request 部分指定所要进行的操作以及操作说要的参数,response 部分表明操作状态,成功或失败。command 和 event 中可能涉及到非基本数据类型,在 domain 中被归为 Type,比如:'frameId': ,其中 FrameId 为非基本数据类型。

至此,不难理解: domain = command + event + type

很多工具都使用了Chrome Debugging Protocol,包括 PhantomJS,Selenium 的 ChromeDriver,本质都是一样的实现,它就相当于 Chrome 内核提供的 API 让应用调用。官网列出了很多有意思的工具:awesome-chrome-devtools/Developing with the protocol,因为 API 丰富,所以才有了这么多的 Chrome 插件。

协议调试

使用 "Protocol Monitor":

image

  • DevTools-on-DevTools

打开 devTools-on-devTools,然后在内部 DevTools 窗口中,使用控制台中的 Main.MainImpl.sendOverProtocol() :

let Main = await import('./main/main.js');
await Main.MainImpl.sendOverProtocol('Emulation.setDeviceMetricsOverride', {
  mobile: true,
  width: 412,
  height: 732,
  deviceScaleFactor: 2.625,
});

const data = await Main.MainImpl.sendOverProtocol("Page.captureScreenshot");

协议定义

参考 devtools-frontend 使用 JSON 定义。

TS 类型文件也是通过脚本读取 JSON 文件生成的:protocol_dts_generator.ts

简单概览:

{
  "domain": "DOM",
  "description": "This domain exposes DOM read/write operations",
  "dependencies": ["Runtime"],
  "types": [
    {
      "id": "NodeId",
      "description": "Unique DOM node identifier.",
      "type": "integer"
    }
    ......
  ],
  "commands": [
    {
      "name": "getAttributes",
      "description": "Returns attributes for the specified node.",
      "parameters": [
        {
          "name": "nodeId",
          "description": "Id of the node to retrieve attibutes for.",
          "$ref": "NodeId"
        }
      ],
      "returns": [
        {
          "name": "attributes",
          "description": "An interleaved array of node attribute names and values.",
          "type": "array",
          "items": {
            "type": "string"
          }
        }
      ]
    }
    ......
  ],
  "events": [
    {
      "name": "childNodeRemoved",
      "description": "Mirrors `DOMNodeRemoved` event.",
      "parameters": [
        {
          "name": "parentNodeId",
          "description": "Parent id.",
          "$ref": "NodeId"
        },
        {
          "name": "nodeId",
          "description": "Id of the node that has been removed.",
          "$ref": "NodeId"
        }
      ]
    }
    ......
  ]
}

关键字解析:

  • domain:协议内容分为若干域 Domain(DOM、Debugger、Network等)。每个域定义了它所支持的许多命令(commands)和它所生成的事件(events)。命令和事件都是固定结构的序列化 JSON 对象。
  • experimental:是否试验性
  • description:描述
  • dependencies:依赖
  • types:Cmmand 和 Event 中涉及到的数据类型的定义
  • commands:如同一个异步调用,通过请求的信息,获取相应的返回结果。这样的通讯必然有一个message id,否则两方都无法正确的判断请求和返回的匹配状况。
  • events:用于由一方单方面的通知另一方某个信息。

Command / Event 命名规范:

  1. 动作:enable、disable、focus
  2. 动词+名词:getAttributes、highlightNode、removeNode、copyTo、

协议示例

🔝{"id":1,"method":"Network.enable","params":{"maxPostDataSize":65536}}
🔝{"id":2,"method":"Page.enable"}
🔝{"id":3,"method":"Page.getResourceTree"}
🔝{"id":4,"method":"Runtime.enable"}
🔝{"id":5,"method":"Profiler.enable"}
🔝{"id":6,"method":"Debugger.enable","params":{"maxScriptsCacheSize":10000000}}
🔝{"id":9,"method":"DOM.enable"}
🔝{"id":10,"method":"CSS.enable"}
⬇️{"method":"Target.attachedToTarget","params":{"sessionId":"93AFA6399DA8A8623D82C166AF8094A2","targetInfo":{"targetId":"9F45931CC38889806267A00B8BB40478","type":"iframe","title":"chrome-extension://react-perf/devtools.html","url":"chrome-extension://react-perf/devtools.html","attached":true,"browserContextId":"35183A5B763F607EACECFD0BBA9421A0"},"waitingForDebugger":false}}
🔝{"id":29,"method":"Page.getResourceTree","sessionId":"93AFA6399DA8A8623D82C166AF8094A2"}
⬇️{"id":1,"result":{}}
⬇️{"id":2,"result":{}}
⬇️{"id":3,"result":{"frameTree":{"frame":{"id":"1071A0A3CD4026CF3910F3BCF58D1171","loaderId":"AF4B4B66A0E7C141C845309F85599A47","url":"devtools://devtools/bundled/devtools_app.html?remoteBase=https://chrome-devtools-frontend.appspot.com/serve_file/@36cc3c133da6270352f9b7ccb712eeacee0dd458/&can_dock=true&toolbarColor=rgba(223,223,223,1)&textColor=rgba(0,0,0,1)&experiments=true","securityOrigin":"devtools://devtools","mimeType":"text/html"},"resources":[{"url":"devtools://devtools/bundled/devtools_app.js","type":"Script","mimeType":"application/javascript","contentSize":195937},{"url":"devtools://devtools/bundled/Images/largeIcons.svg","type":"Image","mimeType":"image/svg+xml","contentSize":20597},{"url":"devtools://devtools/bundled/Images/treeoutlineTriangles.svg","type":"Image","mimeType":"image/svg+xml","contentSize":120},{"url":"devtools://devtools/bundled/Images/smallIcons.svg","type":"Image","mimeType":"image/svg+xml","contentSize":15394},{"url":"devtools://devtools/bundled/shell.js","type":"Script","mimeType":"application/javascript","contentSize":195282}]}}}
⬇️{"method":"CSS.mediaQueryResultChanged","params":{},"sessionId":"93AFA6399DA8A8623D82C166AF8094A2"}

使用 Chrome Protocol monitor Panel 查看:

error

⬇️{"error":{"code":-32000,"message":"Not supported"},"id":20}

错误码定义

未找到相关规范。

协议使用

前端

参考官方推荐的 chrome-remote-interface,接口优雅,异步编程。

const CDP = require('chrome-remote-interface');

async function example() {
    let client;
    try {
        // connect to endpoint
        client = await CDP();
        // extract domains
        const {Network, Page} = client;
        // setup handlers
        Network.requestWillBeSent((params) => { // events
            console.log(params.request.url);
        });
        // enable events then start!
        await Network.enable(); // command
        await Page.enable();
        await Page.navigate({url: 'https://github.com'});
        await Page.loadEventFired();
    } catch (err) {
        console.error(err);
    } finally {
        if (client) {
            await client.close();
        }
    }
}

example();

总的来说,Chrome DevTools Protocol 是 基于 Websocket 的 JSON RPC 协议规范,整个协议的定义、使用及生态还是非常不错,如果有类似的需求,参照着进行设计能够大大提升项目规范性,少踩一些坑。

参考

Pines-Cheng avatar Jul 21 '20 02:07 Pines-Cheng

这里是什么意思,没有明白

打开 devTools-on-devTools,然后在内部 DevTools 窗口中,使用 Main。控制台中的 MainImpl.sendOverProtocol()

MinWest avatar May 19 '21 06:05 MinWest

这里是什么意思,没有明白

打开 devTools-on-devTools,然后在内部 DevTools 窗口中,使用 Main。控制台中的 MainImpl.sendOverProtocol()

@MinWest 笔误,已修正。

Pines-Cheng avatar Jul 27 '21 03:07 Pines-Cheng