玩转 Chrome DevTools,定制自己的调试工具

Chrome DevTools 是我们每天都用的工具,它可以查看元素、网络请求、断点调试 JS、分析性能问题等,是辅助开发的利器。

今天不讲怎么使用它,而是讲一个好玩的方向:定制自己的调试工具。

之前讲过,Chrome DevTools 和 Chrome 是分离的架构,两者之间通过 WebSocket 通信,通信协议是 Chrome DevTools Protocol,简称 CDP:

玩转 Chrome DevTools,定制自己的调试工具

其实这不准确,具体原因后面揭秘。

上图中,UI 的部分叫做 frontend,解析网页、执行 JS 的部分叫做 backend。

backend 是集成在 Chrome 中的,但是 frontend 的部分是独立的。

我们可以从 npm 仓库下载 chrome-devtools-frontend 的代码,我这里用的是 1.0.672485 版本的:

npm install chrome-devtools-frontend@1.0.672485

下载下来的代码有个 front_end 目录,这个就是 Chrome DevTools 的前端代码:

玩转 Chrome DevTools,定制自己的调试工具

它下面有几个 html:

玩转 Chrome DevTools,定制自己的调试工具

我们 “npx http-server .” 起个静态服务看一下:

devtools_app.html 就是网页的那个调试页面:

玩转 Chrome DevTools,定制自己的调试工具

node_app.html 就是 node 的那个调试页面:

玩转 Chrome DevTools,定制自己的调试工具

这就是 Chrome DevTools 的 frontend 部分。

那怎么用这个独立的 frontend 呢?

给它配个 WebSocket 的 backend 不就行了?

用 node 创建个 WebSocket 服务端,打印下收到的消息:

const ws = require('ws');

const wss = new ws.Server({ port8080 });

wss.on('connection'function connection(ws{
    ws.on('message'function message(data{
        console.log('received: %s', data);  
    });
});

在 devtools_app.html 后面加上 ws=localhost:8080 的参数:

玩转 Chrome DevTools,定制自己的调试工具

启动 ws 服务,你就会发现控制台打印了一系列收到的消息:

玩转 Chrome DevTools,定制自己的调试工具

这就是 CDP 协议的数据。

那我们对接一下这个协议,返回相应格式的数据,能在 Chrome DevTools 里做显示么?

我们试一下。

打开 CDP 的文档 https://chromedevtools.Github.io/devtools-protocol/

玩转 Chrome DevTools,定制自己的调试工具

CDP 是按照不同的 Domain 分隔的,比如 DOM、CSS、Debugger 等。

我们找个网络相关的:

玩转 Chrome DevTools,定制自己的调试工具

可能你看到这些协议也不知道怎么用,这时候可以先打开 Chrome DevTools 的 Protocol Monitor 面板,找个网页测试下:

玩转 Chrome DevTools,定制自己的调试工具

看看 NetWork 部分都是怎么通过 CDP 交互的:

玩转 Chrome DevTools,定制自己的调试工具

然后你会发现每次发请求前,backend 都会给 frontend 传一个 Network.requestWillBeSent 的消息,带上这次请求的信息。

那我们能不能也发一个这样的消息呢?

我模拟构造了一个类似的 CDP 消息:

玩转 Chrome DevTools,定制自己的调试工具
ws.send(JSON.stringify({
    method"Network.requestWillBeSent",
    params: {
        requestId`111`,
        frameId'123.2',
        loaderId'123.67',
        request: {
            url'www.guangguangguang.com',
            method'post',
            headers: {
                "Content-Type""text/html"
            },
            initialPriority'High',
            mixedContentType'none',
            postData: {
                "guang"1
            }
        },
        timestampDate.now(),
        wallTimeDate.now() - 10000,
        initiator: {
            type'other'
        },
        type"Document"
    }
}));

然后在 frontend 的页面看一下:

玩转 Chrome DevTools,定制自己的调试工具

你会发现 Network 面板显示了我们发过来的消息!

这就是 Chrome DevTools 的原理。

测试了下 Network 部分的协议之后,我们再来试下 DOM 的。

我用 Protocol Monitor 观察了下 DOM 部分的 CDP 交互:

玩转 Chrome DevTools,定制自己的调试工具

首先通过 DOM.getDocument 获取 root 的信息,这一级返回的 node 只到 body。

然后后面再发 DOM.requestChildNodes 的消息,服务端会回一个 DOM.setChildNodes 的消息来返回子节点的信息。

我们也这样实现一下:

玩转 Chrome DevTools,定制自己的调试工具

收到 DOM.getDocument 的消息的时候,我们返回 root 的信息,只到 body 那一级。

然后发送 DOM.setChildNotes 来返回子节点的信息。

还要处理下 DOM.requestChildNodes 的消息,返回空就行。

完整代码如下:

ws.on('message'function message(data{
        console.log('received: %s', data);

        const message = JSON.parse(data);
        if (message.method === 'DOM.getDocument') {
            ws.send(JSON.stringify({
                id: message.id,
                result: {
                    root: {
                        nodeId1,
                        backendNodeId1,
                        nodeType9,
                        nodeName"#document",
                        localName"",
                        nodeValue"",
                        childNodeCount2,
                        children: [
                            {
                                nodeId2,
                                parentId1,
                                backendNodeId2,
                                nodeType10,
                                nodeName"html",
                                localName"",
                                nodeValue"",
                                publicId"",
                                systemId""
                            },
                            {
                                nodeId3,
                                parentId1,
                                backendNodeId3,
                                nodeType1,
                                nodeName"HTML",
                                localName"html",
                                nodeValue"",
                                childNodeCount2,
                                children: [
                                    {
                                        nodeId4,
                                        parentId3,
                                        backendNodeId4,
                                        nodeType1,
                                        nodeName"HEAD",
                                        localName"head",
                                        nodeValue"",
                                        childNodeCount5,
                                        attributes: []
                                    },
                                    {
                                        nodeId5,
                                        parentId3,
                                        backendNodeId5,
                                        nodeType1,
                                        nodeName"BODY",
                                        localName"body",
                                        nodeValue"",
                                        childNodeCount1,
                                        attributes: []
                                    }
                                ],
                                attributes: [
                                    "lang",
                                    "en"
                                ],
                                frameId"3A70524AB6D85341B3B613D81FDC2DDE"
                            }
                        ],
                        documentURL"http://127.0.0.1:8085/",
                        baseURL"http://127.0.0.1:8085/",
                        xmlVersion"",
                        compatibilityMode"NoQuirksMode"
                    }
                }
            }));

            ws.send(JSON.stringify({
                method"DOM.setChildNodes",
                params: {
                    nodes: [
                        {
                            attributes: [
                                "class",
                                "guang"
                            ],
                            backendNodeId6,
                            childNodeCount0,
                            children: [
                                {
                                    backendNodeId6,
                                    localName"",
                                    nodeId7,
                                    nodeName"#text",
                                    nodeType3,
                                    nodeValue"光光光",
                                    parentId6,
                                }
                            ],
                            localName"p",
                            nodeId6,
                            nodeName"P",
                            nodeType1,
                            nodeValue"",
                            parentId5
                        }
                    ],
                    parentId5
                }
            }));
        } else if (message.method === 'DOM.requestChildNodes') {
            ws.send(JSON.stringify({
                id: message.id,
                result: {}
            }));
        }
    });

返回的内容如上,我们返回了一个 P 标签,有 class 属性,还有一个文本节点。

重启下 backend 服务,在 frontend 里重连一下,你就会发现 frontend 显示了我们返回的 DOM 信息:

玩转 Chrome DevTools,定制自己的调试工具

经过这两个案例,我们就搞明白了 Chrome DevTools frontend 是怎么和 backend 交互的。

看到自己模拟 DOM 信息这部分,不知道你是否会想到跨端引擎呢。

跨端引擎就是通过前端的技术来描述界面(比如也是通过 DOM),实际上用安卓和 IOS 的原生组件来做渲染。

它的调试工具也是需要显示 DOM 树的信息的,但是因为并不是网页,所以不能直接用 Chrome DevTools。

那如何用 Chrome DevTools 来调试跨端引擎呢?

看完上面两个案例,相信你就会有答案里。只要对接了 CDP,自己实现一个 backend,把 DOM 树的信息,通过 CDP 的格式传给 frontend 就可以了。

自定义的调试工具几本都是前端部分集成下 Chrome DevTools frontend,后端部分实现下对接 CDP 的 ws 服务来实现的。

跨端引擎的调试工具我们知道怎么实现了,那小程序引擎呢?

小程序引擎的调试工具更简单,因为它实际上渲染是用的网页,有 CDP 的 backend,可以直接和 frontend 对接,不用自己实现 CDP 交互。

我下载了 vivo 的快应用开发工具,它有编辑器、调试器、模拟器这几部分:

玩转 Chrome DevTools,定制自己的调试工具

模拟器渲染的内容能够在调试器里调试,这也是通过 WebSocket 通信的么?

其实不是,Chrome DevTools 支持几种信道,WebSocket 是最常见的一种,还有就是嵌入的时候会通过全局函数通信,electron 会通过 ipc 的方式通信等等。

比如 WebSocket 时的通信实现是这样的:

玩转 Chrome DevTools,定制自己的调试工具

而 electron 环境下是这样的:

玩转 Chrome DevTools,定制自己的调试工具

嵌入到一个环境的时候是这样的:

玩转 Chrome DevTools,定制自己的调试工具

这也是为什么文章最开始我说 Chrome DevTools 和 Chrome 通过 WebSocket 通信是不准确的,其实是通过全局函数的方式。

而且,像上面那种在一个窗口里渲染,在另一个窗口里调试的这种需求,electron 直接提供了 api 来支持。

玩转 Chrome DevTools,定制自己的调试工具

使用 setDevToolsWebContents 的 api,就可以让 devtools 的 frontend 显示在任意的窗口里。

所以说,小程序的调试工具实现起来还是很简单的,不但 CDP 交互不用自己实现,而且一个窗口渲染,一个窗口显示Chrome DevTools frontend 这种功能 electron 都已经提供了。

上面我们都是自己实现的 backend,那能自己实现 frontend 么?

当然也是可以的。

我们通过命令行的方式把 chrome 跑起来,通过 remote-debugging-port 指定 backend 的端口:

/Applications/Google Chrome.app/Contents/MacOS/Google Chrome --remote-debugging-port=9222

然后实现个 WebSocket 客户端连上就可以了。

当然自己实现 CDP 的交互还是挺麻烦的,chrome 给提供了一个工具包 chrome-remote-interface,可以用 api 的方式来组织代码。

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

async function test({
    let client;
    try {
        client = await CDP();
        const { Page, DOM, Debugger } = client;
        //...
    } catch(err) {
        console.error(err);
    }
}
test();

我们测试一下 DOM 部分的协议:

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

async function test({
    let client;
    try {
        client = await CDP();
        const { Page, DOM, Debugger } = client;

        await Page.enable();
        await Page.navigate({url'https://baidu.com'});

        await DOM.enable();

        const { root } = await DOM.getDocument({
            depth-1
        });
        
    } catch(err) {
        console.error(err);
    }
}
test();

打个断点,看下 backend 返回的消息:

玩转 Chrome DevTools,定制自己的调试工具

是不是很熟悉?

不过这次是真实的 DOM.getDocument 的消息。

我们自己实现了 frontend,对接了真实 backend,之前也自己实现了 backend,对接了真实 frontend。

那能不能自己实现 frontend,对接自己实现的 backend 呢?

当然可以,不过这样就没必要用 CDP 了,自己创建一套协议不香么?

其实 Vue DevTools 和 React DevTools 就是自己定制的一套协议。

它们都是以 Chrome 插件的方式存在的,我们要先了解下 Chrome 插件,准确的说是 Chrome DevTools 插件:

玩转 Chrome DevTools,定制自己的调试工具

它包含三部分:content script、background page、 devtools page。

content script 是可以获取 DOM 的,但是不能访问用户的 JS。这很容易理解,获取 DOM 是插件需要的功能,但是为了安全,又限制了只能访问 DOM。

background page 随浏览器打开就启动,浏览器关闭才销毁,存在周期很长。这也很容易理解,插件是需要这么长的存在周期的,完成一些跨页面的功能。

devtools page 就是在 DevTools 的新 Tab 显示的页面了,它还可以向页面注入 JS。

content script 和 devtools page 都可以和 background page 通信。

那基于这些功能,怎么实现一个自定义调试工具呢?

调试工具主要是 frontend、backend,再就是通信协议。

很容易想到可以这样实现:

devtools page 像页面注入 backend.js,用来获取运行时的信息,然后传递给 devtools page。

devtools page 做 frontend 的显示。

两者之间的通信协议可以自定义。

vue devtools 就是这样实现的:

玩转 Chrome DevTools,定制自己的调试工具

你可以看到它的代码分包:

玩转 Chrome DevTools,定制自己的调试工具

backend 就是注入到页面的 js,frontend 部分就是 devtools page 的显示和交互的实现。

react devtools 也是差不多的原理。

玩转 Chrome DevTools,定制自己的调试工具

只不过它还有 electron 的版本,用于 React Native 的调试:

玩转 Chrome DevTools,定制自己的调试工具

至此,怎么基于 Chrome Devtools 自定义调试工具,如何基于 devtools extension 实现调试工具我们都了解了。

再回头看下 CDP:

调试工具我们知道怎么实现了,那 CDP 只能用来调试么?

也不是,其实也可以起到远程控制的作用。

puppeteer 就是基于 CDP 实现的自动化测试,它的原理是内置了一个 chromium,用调试模式启动,会有一个 ws 的 backend 的端口。然后用自己实现的 frontend 连接上,通过 CDP 来控制它。

这就是 puppeteer 自动化测试的原理,只不过它是在 node 环境下的。

浏览器环境能实现这种控制么?

也是可以的,Chrome 插件提供了 debugger 的 api,可以代替 frontend 来给 backend 发消息,从而控制浏览器:

玩转 Chrome DevTools,定制自己的调试工具

其实这个和 puppeteer 的原理很像了,只不过是在浏览器里的。

有一个叫做 puppeteer IDE 的 chrome 插件,就是通过 debugger 来实现了 puppeteer 的 api,从而可以在控制台写 puppeteer 的自动化测试脚本,然后执行。

玩转 Chrome DevTools,定制自己的调试工具

感兴趣可以去玩一下。

总结

Chrome DevTools 分为 frontend、backend,之间通过 Chrome DevTools Protocol 通信,通信的信道有很多种,常用的是 WebSocket。

我们可以集成 chrome devtools frontend 的代码,对接自己实现的 backend,从而实现调试的功能。跨端引擎的调试就是这样实现的。

小程序引擎调试工具的实现更简单,CDP 不用自己实现,electron 还提供了在一个窗口显示另一个窗口的 devtools frontend 的 api 可以直接用。

除了自己实现 backend,我们也可以自己实现 frontend,通过 chrome-remote-interface 这个包可以用 api 来操作 CDP。

当然,像 Vue DevTools、React DevTools 这种都是要自定义调试协议的,他们的实现原理是 devtools page 向页面注入了 background 代码,之间通过一定的协议通信,然后在 devtools 里面做渲染。

除了调试之外,CDP 还能实现远程控制, puppeteer 就是通过 CDP 实现的自动化测试。

chrome 插件的 debugger api 也可以发送 CDP 消息,可以实现和 puppeteer 类似的效果。

其实调试还是挺简单的,就是 frontend、backend、调试协议,然后可能有很多种信道,不管是 Chrome DevTools 还是自定义调试工具都是这样。

自己做一个调试工具的话,可以集成 Chrome DevTools frontend,然后对接 backend。可以通过 devtools extension 扩展,往页面注入 backend 代码。也可以基于 electron 实现一个完全独立的调试工具。

原文始发于微信公众号(神光的编程秘籍):玩转 Chrome DevTools,定制自己的调试工具

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/109176.html

(0)
神光的编程秘籍的头像神光的编程秘籍

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!