手把手带你了解DevTools实现原理

没有DevTools的前端就像基督教徒没有耶路撒冷。

前言

经过二十年的发展,我们的调试工具已经渐渐从最初的在IE时代的window.alert()调试,这种调试方式,不可避免的就会有极低的调试效率。到2006AppleWebKit团队发布第一代调试工具后面FireFox发布早期调试神器FireBug。直到互联网的发展逐渐来到了移动互联网时代,调试工具届出现了一个大爹那就是ChromedevTools,它能够支持远程真机调试在那以后调试工具的发展进程和devTools的发展进程几乎画上等号。

调试工具也确实给前端开发者带来了极大的好处,已经成为了目前不可缺少的工具。

DevTools剖析

💡 JavaScript实现 & 实际上是个网页 \ Google:“浏览器不就是跑JS的么?不用JS用什么?”

架构:CS(Client-Server)架构

数据封装协议:CDP(Chrome DevTools Protocol )

image.png 一般的DevTools过程

image.png 安卓端的过程

工具的四个组成部分:

  • Fontend: 前端,用户操作的界面
  • Backend: 后端,一般是 Chromium、V8 或 Node.js
  • Protocol: 调试协议(JSON 格式的数据封装协议,包含了HTTP和WebSocket协议)
  • Message Channels:调试消息通道

image.png 源码片段

值得一提的是,当初devTools决定使用WebSocket的时候,WebSocket还仅仅只在实验阶段,由此可见国际大厂的工程师的眼光确实长远\~

如何在devTools中看到CDP传输的信息呢

开启工具

设置中打开工具 image.png

使用工具 image.png

此时此刻就可以看到请求的入参和出参等信息,前面有提到CDP是基于JSON的,这里我们就可以看到Response中的返回的就是JON格式 image.png

发出CDP命令

image.png 我们还可以使用 Protocol Monitor(版本 92.0.4497.0+)发出自己的命令。

如果该命令不需要任何参数,请在“协议监视器”面板底部的提示符中键入该命令,然后按 Enter 键,例如:

Page.captureScreenshot

如果命令需要参数,请以 JSON 形式提供,例如:

{"command":"Page.captureScreenshot","parameters":{"format": "jpeg"}}.

简析CDP协议

CDP文档

什么是CDP?

Chrome DevTools Protocol的数据交互是通过WebSocket进行的。WebSocket是一种全双工通信协议,可以在单个TCP连接上进行双向通信。通过WebSocket,前端和后端之间可以进行实时通信,数据可以在两个方向上实时传输。

在Chrome DevTools Protocol中,前端通过WebSocket向后端发送命令和数据。后端接收到命令和数据后,会根据命令执行相应的操作,并将结果通过WebSocket返回给前端。前端接收到结果后,会将其显示在用户界面上,或者执行相应的操作。

在数据交互过程中,Protocol使用了JSON格式的数据进行传输。JSON是一种轻量级的数据交换格式,易于人类阅读和编写,也易于机器解析和生成。在前端和后端之间传输的数据都是JSON格式的字符串。

协议是如何定义的?

规范协议定义位于 Chromium 源代码树中:(browser_protocol.pdl 和js_protocol.pdl)。它们由 DevTools 工程团队手动维护。声明性协议定义跨工具使用;例如,在 Chromium 中创建了一个绑定层,供 Chrome DevTools 与之交互,并为 Chrome Headless 的 C++ 接口单独生成绑定。

CDP的数据是如何组成的?

之前有说到过CDP的数据是基于JSON的,所以从前端传到后端的数据格式是这样的应该没问题吧

json { "id":1, "method":"Network.enable", "params":{"maxPostDataSize":65536} }

这个时候我们可以观察到到method中的格式居然是用点来调用的,可是这是个字符串啊是不是有点不合理?

对就是不合理!所以是需要处理一下的。

这里的method是用.来分割的,前面的部分是domain(域)当然不是域名,可以理解为作用域。后面跟着的才是真正的调用的对应方法。

比如Network的方法们

image.png 这时候有的同学可能就会问了:“啊?那么土?用split('.')来分割?”

对的,源码里也是这么干的。

image.png

开始实现

下载源码

下载并且打开源码 baseh npm install chrome-devtools-frontend@1.0.672485

后端部分

Chrome中自带的后端

前面说了Chrome自带后端,但是我们如何才能够去感知到这个后端呢?

打开远程调试,指定端口 bash sudo /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9527

这个时候启动的Chrome就是作为一个Server host的web app。

地址栏中输入:localhost:9527/jsonor /json/list image.png 我们可以看到当前浏览器打开的tab页,以及页面的id,inspect地址等等内容,由此我们可以感知到浏览器中后端的存在。(当然在后面自己实现前端的部分会更加明显的感知到)

inspector.html和Chrome host之间通过webSocket建立连接,这个ws地址就是url中ws参数的值。其中55A4F84F6A66845F72388146E3B8F986是page id,每个页面都有一个唯一的page id,chrome就是通过这个id确定哪个是目标页面。

我们进入到该调试页面中,打开调试页面即可看到ws链路。证明了该页面在与Chrome后端进行ws的数据交互。 image.png 并且chrome调试器实例和目标页面实例之间是进程通信,所以inspector.html可以通过chrome调试器实例加载目标页面的source文件,还可以操作目标页面,例如加断点、刷新、记录Network信息等。

HTTP端

// 关闭一个标签页,传入该页面的id
<http://localhost:9527/json/close/7FBA9CF445D4BF16990FEF94A6F32F76>

// 激活标签页
<http://localhost:9527/json/activate/7FBA9CF445D4BF16990FEF94A6F32F76>

// 查看chrome和协议的版本信息
<http://localhost:9527/json/version>

// 查看使用的协议内容
<http://localhost:9527/json/protocol>

// 浏览器自带的调试工具
/devtools/inspector.html

// 协议的ws端点
WebSocket: /devtools/page/{targetId}

悟了

笔者作为一个前端开发者,理解到这里的时候突然悟了。

💡 了解完这段我对浏览器有了新的理解,浏览器的实质是个巨型桌面应用。\ 那前端开发者的实质就是在浏览器游戏的开放规则下的玩家。

那浏览器本身是不是就是一个CS架构?

浏览器的后端是不是可以理解为是一个巨大的中间层?

那如果需要完全打通devtools的全部原理和流程的话还需要加入一些浏览器实现原理的部分。

手搓devTools后端

我们已经启了一个前端网页了,但是俗话说得好上阵父子兵,一个前端并不能达到调试的效果,并且我们需要去观察后端是怎么运作的,最好的方法就是——自己启一个node服务

(如何打开图片页面在后文打开fontend文件的部分) image.png

接下来在url的后面加上?ws=localhost:port 使该页面使用我们自己写的后端。

后端首先要安装WebSocket ```javascript const ws = require('ws'); const wss = new ws.Server({port: 8988}); console.log('启动node程序') wss.on('connection',function connection(ws) { ws.on('message', function message(data) { console.log('收到数据: %s', data);

const message = JSON.parse(data);
        if (message.method === 'DOM.getDocument') {
            ws.send(JSON.stringify({
                id: message.id,
                result: {
                    root: {
                        nodeId: 1,
                        backendNodeId: 1,
                        nodeType: 9,
                        nodeName: "#document",
                        localName: "",
                        nodeValue: "",
                        childNodeCount: 2,
                        children: [
                            {
                                nodeId: 2,
                                parentId: 1,
                                backendNodeId: 2,
                                nodeType: 10,
                                nodeName: "html",
                                localName: "",
                                nodeValue: "",
                                publicId: "",
                                systemId: ""
                            },
                            {
                                nodeId: 3,
                                parentId: 1,
                                backendNodeId: 3,
                                nodeType: 1,
                                nodeName: "HTML",
                                localName: "html",
                                nodeValue: "",
                                childNodeCount: 2,
                                children: [
                                    {
                                        nodeId: 4,
                                        parentId: 3,
                                        backendNodeId: 4,
                                        nodeType: 1,
                                        nodeName: "HEAD",
                                        localName: "head",
                                        nodeValue: "",
                                        childNodeCount: 5,
                                        attributes: []
                                    },
                                    {
                                        nodeId: 5,
                                        parentId: 3,
                                        backendNodeId: 5,
                                        nodeType: 1,
                                        nodeName: "BODY",
                                        localName: "body",
                                        nodeValue: "",
                                        childNodeCount: 1,
                                        attributes: []
                                    }
                                ],
                                attributes: [
                                    "lang",
                                    "en"
                                ],
                                frameId: "3A70524AB6D85341B3B613D81FDC2DDE"
                            }
                        ],
                        documentURL: "<http://127.0.0.1:8080/>",
                        baseURL: "<http://127.0.0.1:8080/>",
                        xmlVersion: "",
                        compatibilityMode: "NoQuirksMode"
                    }
                }
            }));

            ws.send(JSON.stringify({
                method: "DOM.setChildNodes",
                params: {
                    nodes: [
                        {
                            attributes: [
                                "class",
                                "devToolsClass"
                            ],
                            backendNodeId: 6,
                            childNodeCount: 0,
                            children: [
                                {
                                    backendNodeId: 6,
                                    localName: "",
                                    nodeId: 7,
                                    nodeName: "#span",
                                    nodeType: 3,
                                    nodeValue: "页面的文字",
                                    parentId: 6,
                                }
                            ],
                            localName: "div",
                            nodeId: 6,
                            nodeName: "DIV",
                            nodeType: 1,
                            nodeValue: "",
                            parentId: 5
                        }
                    ],
                    parentId: 5
                }
            }));
        } else if (message.method === 'DOM.requestChildNodes') {
            ws.send(JSON.stringify({
                id: message.id,
                result: {}
            }));
        }

    })

    ws.send(JSON.stringify({
        method: "Runtime.consoleAPICalled",
        params: {
            type: "log",
            args: [
                {
                    type: "string",
                    value: "被输出了"
                }
            ],
            executionContextId: 92,
            timestamp: 1694608920078.64,
            stackTrace: {
                callFrames: [
                    {
                        functionName: "",
                        scriptId: "5500",
                        url: "",
                        lineNumber: 0,
                        columnNumber: 8
                    }
                ]
            }
        },
    }))

    ws.send(JSON.stringify({
        method: "Network.requestWillBeSent",
        params: {
            requestId: `10088`,
            frameId: '123',
            loaderId: '12388',
            request: {
                url: 'www.zhangg0.com',
                method: 'post',
                headers: {
                    "Content-Type": "text/html"
                },
                initialPriority: 'High',
                mixedContentType: 'none',
                postData: {
                    "name": 1
                }
            },
            timestamp: Date.now(),
            wallTime: Date.now() - 10000,
            initiator: {
                type: 'other'
            },
            type: "Document"
        }
    }));

})

``` 我们在上述操作中我们会收到前端传递的消息并且打印在控制台中,并且会在Element中显示DOM结构,在Network中会发送一条请求。我们验证一下是否和我们预想的一样。

刷新一下前端页面

image.png

image.png

image.png 在上述部分中我们可以看到inspect和控制台中出现的情况与我们预计的相同。

至此,手搓后端成功。

前端部分

打开fontend文件

我们在fontend文件夹下就可以看到devtools_app.html,node_app.html,很好这个名字一看就是个主文件,所以我们起一个npx http-server . 静态服务看看情况。打开对应的页面就可以看到我们的调试工具了。

image.png

对的,它是个网页,我们甚至可以在调试工具里打开调试工具

image.png

打开之后我们就可以看到我们的Protocol Monitor一直在与后端信息交互,那么问题来了?后端在哪里?我不是只启了前端么??后端呢??后端已经集成在chromium中了,以此来形成CS架构。

其实这个网页也集成在Chrome了devtools://devtools/bundled/inspector.html

手搓前端

首先我们需要有一个后端,前文有提到过打开Chrome的后端,我们使用相同的方式 javascript sudo /Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9527 这个时候会跳出来页面。

因为我们使用Chrome的后端,所以我们就要遵守其协议,首先要安装一下这个包chrome-remote-interface ```javascript const CDP = require('chrome-remote-interface');

async function myDevToolsFE() {
    let client;
    try {
        client = await CDP({
            host: 'localhost',
            port: 9527
        });
        const { Page, DOM, Debugger, Runtime, CSS, Profiler } = client;
        console.log(Page, DOM, Debugger, Runtime, CSS, Profiler);
    } catch(err) {
        console.error(err);
    }
}
myDevToolsFE();

``` 把各个域的内容打印出来,如下。就是CDP协议中的域和方法。 image.png

我们在前端开始使用一些CDP的方法 ```javascript const CDP = require('chrome-remote-interface');

async function myDevToolsFE() {
    let client;
    try {
        client = await CDP({
            host: 'localhost',
            port: 9527
        });
        const { Page, DOM, Debugger, Runtime, CSS, Profiler,Network } = client;
        console.log(Page, DOM, Debugger, Runtime, CSS, Profiler);
        await Network.enable();
        //网络  requestWillBeSent->当页面即将发送HTTP请求时触发。
        Network.requestWillBeSent((params) => {
            console.log('发起请求' + params.request.url)
        });

        await Page.enable();
        await Debugger.enable();
        await DOM.enable();
        await CSS.enable();

        CSS.on('styleSheetAdded', async (event) => {
            debugger;
            const styleSheetId = event.header.styleSheetId;
            const content = await CSS.getStyleSheetText({ styleSheetId });

            cssMap.set(styleSheetId, {
                meta: event.header,
                content: content.text
            });
        })
        Debugger.on('scriptParsed', async (event) => {
            debugger;
            const scriptId = event.scriptId;
            const content = await Debugger.getScriptSource({ scriptId });

            jsMap.set(scriptId, {
                meta: event,
                content: content.scriptSource
            });
        })

        await Page.enable()
        await Page.navigate({url: '<http://localhost:9527/json/protocol>'})

        const res = await Page.captureScreenshot()

    } catch(err) {
        console.error(err);
    }
}
myDevToolsFE();

``` 后端传给我们的CSS属性 image.png

页面也进行了跳转

image.png 至此,我们的前端就搓完了(授人以鱼不如授人以渔,已经教了怎么搓就是已经搓完了)。

到这里就发现了一个很有趣的冷知识,之前在console里面直接输入指令就能实现还以为控制台里集成了,了解了调试工具原理之后才发现原来他只是个传话筒罢了~

如何自己搓一个DevTools

前面已经给大家搓了前端和后端,把我们自己搓的前后端拼接在一起那就是我自己搓的DevTools了。

我们知道CDP它们由 DevTools 工程团队手动维护

DevTools的主要组成实际上就是前端后端和CDP协议,那前后端我们都自己搓了,接下来只需要自己定义一套协议,并且完成里面的函数方法之后,你就拥有了一个完全由自己开发的devTools了。这边由于篇幅有限就不贴代码了(狗头)。

学这个有什么用??

devTools在那么多年的迭代下已经非常的完善,我们如果需要去手搓一个devTools明显是不够现实的,所以有的同学可以就会问我们学这个到底有什么用?

其实学的时候因为论坛上没有很多能比较完整的且能形成闭合且看起来容易入门理解的文章,导致我在这个过程中遇到了很多的瓶颈我也在问自己为什么?其实去了解这个的原因就是因为我自己多问了个为什么。。。。

  1. 了解了实现原理,可能自己某些当前devtools无法覆盖的情况下去实现自己的一个devtools或其中的某个功能,例如vue or react 的调试插件。
  2. 了解浏览器的深层原理

结语

老师从小就教说要多问几个为什么,这些年来我发现一个问题,就是为什么问的越多就会越觉得自己菜哈哈哈

参考

以及掘金及国内外各大技术论坛的各种文章。。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: DevTools(开发者工具)是一组工具,可以帮助开发者更轻松地构建、调试和优化网络应用程序。它们可以帮助开发者更快地识别和解决问题。DevTools还可以让开发者实现更多的功能,比如实时预览和检查网页布局,查看网络请求和资源,以及更快地调试 JavaScript 代码。 ### 回答2: 开发者工具(DevTools)是现代浏览器提供的一个功能强大的工具集,用于在开发和调试网页时进行查看、分析和修改。 DevTools的工作原理可以大致分为以下几个步骤: 1. 嵌入到浏览器中:开发者工具是以插件或扩展的形式嵌入到浏览器中的,提供给开发人员使用。用户可以通过浏览器的菜单或快捷键打开开发者工具。 2. 与浏览器建立通信:开发者工具与浏览器建立通信渠道,通过该通道获取当前页面的DOM结构、CSS样式和JavaScript运行情况等信息。 3. 查看页面结构:开发者工具中的“元素”面板可以显示当前页面的DOM结构,开发者可以在该面板中查看并编辑HTML标签、属性和样式。 4. 调试JavaScript代码:开发者工具中的“控制台”面板可以提供一个JavaScript的运行环境,开发者可以在该面板中编写和调试JavaScript代码,查看代码执行的结果和错误信息。 5. 分析网络请求:开发者工具中的“网络”面板可以查看当前页面发送的网络请求,包括请求的URL、请求方法、请求头和响应内容等信息,方便开发者分析页面加载和数据传输的性能问题。 6. 性能分析和优化:开发者工具中的“性能”面板可以用于分析页面的加载性能,并提供诸如JavaScript性能和内存使用情况的详细报告,帮助开发者进行优化。 7. 其他功能:开发者工具还提供了诸如查看和调试页面的样式、查找和替换文本、模拟设备和网络等功能,帮助开发者进行更全面和高效的调试和开发。 总之,开发者工具通过与浏览器建立通信渠道,获取并展示当前页面的各种信息,为开发者提供了丰富的调试和分析功能,帮助他们更好地开发和优化网页。 ### 回答3: 开发者工具(DevTools)是一种集成在浏览器中的工具,用于帮助开发人员在开发和调试网页时进行分析、监控和修改。DevTools的原理涉及以下几个方面。 首先,DevTools与浏览器紧密集成,可以直接访问浏览器中的内部数据结构和功能接口。通过与渲染引擎(如WebKit或Chromium)的通信,DevTools能够获取DOM树结构、CSS样式表、JavaScript堆栈信息等页面相关的数据信息。 其次,DevTools利用浏览器的调试接口(如Chrome的Chrome DevTools Protocol)与浏览器进行通信。这些接口提供了许多底层功能,例如获取网络请求的详细信息、监测性能指标、执行JavaScript代码、模拟用户行为等。DevTools可以通过发送命令和接收响应来与浏览器进行交互,以实现对页面的分析和修改。 还有一些与开发者工具密切相关的技术,例如断点调试和代码审查。开发者工具可以在页面中设置断点,当特定条件满足时,自动中断JavaScript的执行,并提供调试信息和堆栈跟踪,帮助开发人员定位问题。此外,开发者工具还提供了代码审查功能,用于分析和修改页面的HTML、CSS和JavaScript代码。 总之,开发者工具的原理是通过与浏览器内部通信,获取页面的各种数据信息,并提供操作界面和调试功能,以帮助开发人员进行网页开发和调试。开发者工具的实现依赖于浏览器的内部结构和接口,以及相关的调试和审查技术。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值