朋友们,一起学习下 Chrome DevTools Protocol。
1. Debug 起源
1947 年 9 月 9 日,第一代程序媛大佬 Hopper 正领着她的小组在一间一战时建造的老建筑机房里构造一个称为“Mark II”的艾肯中继器计算机。
那是一个炎热的夏天,房间没有空调,所有窗户都敞开散热。突然 Mark II 死机了,操作人员在电板编号为 70 的中继器触点旁发现了一只飞蛾。操作员把飞蛾贴在操作日志上,并写下了“First actual case of bug being found”,他们还提出了一个词:“debug(调试)”,表示他们已经从机器上移走了bug(调试了机器)。
于是,引入了一个新的术语“debugging a computer program(调试计算机程序)”。
2. DevTools (Debugging Tools) 发展史
在 2006 年前的 IE 时代,调试 JavaScript 代码主要靠 window.alert() 或者将调试信息输出到页面上,这种硬 debug 的手段,不亚于系统底层开发,效率极低。
2006 年 1 月份,Apple 的 WebKit 团队第一版本的 Web Inspector 问世,尽管最初版的调试工具很简陋(它甚至连 console 都没有),但是它为开发者展示了两个他们很难洞见的内容——DOM 树以和与之匹配的样式规则。
这奠定了今后多年的网页调试工具的原型。
同年 4 月,以最大的食虫植物命名的 Drosera 发布,它可以给任何的 WebKit 应用添加断点,调试 JavaScript——不仅限于是 Safari。
同时开源社区出现了一款 Firefox 的插件 Firebug,专注于 Web 开发的调试,它是在 Chrome 全世界最好的前端调试工具,同时也奠定了现代 DevTools 的 Web UI 的布局。
Firebug 早期版本就已经支持了 JavaScript 的调试,CSS Box 模型可视化展示,HTTP Archive 的性能分析等优秀特性,后来的 DevTools 参考了此插件的功能和产品定位。
2016 年 Firebug 整合到 Firefox 内置调试工具。
2017 年 Firebug 停止更新,一代神器就此谢幕。
此后开源界的狠角色 Google 团队基于 WebKit 加入浏览器研发,他们推出的 Chrome 以「安全、极速、更稳定」吸引了大部分开发者的关注,同时在开发者工具这方面, Google 吸收多款调试工具的优秀功能,推出了 DevTools。
虽然当时的界面相比如今,十分简陋,但此后 DevTools 的发展基本就与 Chrome DevTools 的发展史划等号了。
当然,不管是 Firebug 还是后来基于 Webkit(早期)、 Blink (现今) 内核的 Chrome ,再或者是 2016 年后的 node-inspector ,他们都离不开 Web Inspector,更多详细的 Web Inspector 发展史可以参考 10 Years of Web Inspector。
3. DevTools 架构
DevTools 是 client-server
架构:
client 端提供可视化 Web UI 界面供用户操作,它负责接收用户操作指令,然后将操作指令发往浏览器内核或 Node.js 中进行处理,并将处理结果数据展示在 Web UI 上。
server 端启动了两类服务:
- HTTP 服务: 提供内核信息查询能力,比如获取内核版本、获取调试页的列表、启动或关闭调试。
- WebSocket 服务:提供与内核进行真实数据通信的能力,负责 Web UI 传递过来的所有操作指令的分发和处理,并将结果送回 Web UI 进行展示。
3.1 Chrome DevTools
以上具体化到 Chrome 开发者工具,你一定倍感亲切。
Chrome DevTools 提供了一套内置于 Google Chrome 中的 Web 开发和调试工具,可用来对网站进行迭代、调试和分析。
Chrome DevTools 主要由四部分组成:
- Frontend:调试器前端,默认由 Chromium 内核层集成,DevTools Frontend 是一个 Web 应用程序;
- Backend:调试器后端,一般是 Chromium、V8 或 Node.js;
- Protocol:调试协议,调试器前后端将遵守该协议进行通信。 它分为代表被检查实体的语义方面的域。 每个域定义类型、命令(从前端发送到后端的消息)和事件(从后端发送到前端的消息)。该协议基于 json rpc 2.0 运行;
- Message Channels:调试消息通道,消息通道是调试前后端间发送协议消息的一种方式。包括:Embedder Channel、WebSocket Channel、Chrome Extensions Channel、USB/ADB Channel。
总结来说,本质上 Chrome DevTools 就是一个 Web 应用程序,它通过使用 Chrome DevTools Protocol 与后端进行交互,达到调试目的。
关于 Chrome 开发者工具的详细使用可以看官方文档。
接下来聚焦 DevTools 的核心:Protocol 。
4. Chrome DevTools Protocol
CDP 本质就是一组 JSON 格式的数据封装协议,JSON 是轻量的文本交换协议,可以被任何平台任何语言进行解析。
4.1 定义
以 Tracing 的协议为例:
{"domain": "Tracing","experimental": true,"dependencies": ["IO"],"types": [{"id": "TraceConfig","type": "object","properties": [{"name": "recordMode","description": "Controls how the trace buffer stores data.","optional": true,"type": "string","enum": ["recordUntilFull","recordContinuously","recordAsMuchAsPossible","echoToConsole"]},...]},...],"commands": [{"name": "start","description": "Start trace events collection.","parameters": [{...}]},{"name": "end","description": "Stop trace events collection."},...],"events": [{"name": "tracingComplete","description": "Signals that tracing is stopped and there is no trace buffers pending flush, all data were\ndelivered via dataCollected events.","parameters": [{"name": "dataLossOccurred","description": "Indicates whether some trace data is known to have been lost, e.g. because the trace ring\nbuffer wrapped around.","type": "boolean"},...]}]
}
- domain:协议把操作划分为不同的 domain(DOM、Console、Network 等,可以理解为 DevTools 中不同的功能模块)。每个 domain 内还定义了他们支持的命令(commands)和事件(events)以及相关类型(types)的具体结构
- experimental:该 domain 是否属于实验性
- description:domain 的功能描述
- dependencies:domain 的依赖
- commands:如同异步调用,对应 socket 通信的请求/响应模式,包含 request/response,通过请求信息,获取相应返回结果,通讯需要有 message id
- events:发生的事件信息,对应 socket 通信的发布/订阅模式,用于发送通知信息
- types:domain 包含的 commands 和 events 数据类型定义
4.2 调试
如下图在 Chrome DevTools 中操作了 Performance 的录制,可以在 Chrome 中开启 Protocol monitor 查看具体的通讯信息。
每个 Method (${domain}.${conmand}
)包含 request 和 response 两部分,request 部分指定所要进行的操作以及操作说要的参数,response 部分表明操作状态,成功或失败。
除了使用 Protocol Monitor,还可以参考 https://stackoverflow.com/questions/12291138/how-do-you-inspect-the-web-inspector-in-chrome/12291163#12291163,开启对 Chrome DevTools 的调试。
4.3 使用
官方推荐的支持 CDP 的 Libraries 多达近十种语言。
Google 官方推荐了 Node.js 版本 Puppeteer ,通过 Puppeteer 完整地实现了 CDP 协议,为 Chrome 内核通信的方式打了一个样,接着开源世界陆续推出了多个语言版本的 CDP 的使用库。
4.3.1 Puppeteer
Puppeteer 是一个 Node 库,它提供了一个高级 API 来通过 CDP 协议控制 Chrome 或 Chromium。
Puppeteer 怎么用,我就不多写了(如果英文文档看不懂,咱就看中文文档)。
我可能更偏向于结合源码理解它是如何做到与 CDP 关联,并且如何发挥作用的。
还是以 Tracing 为例
const puppeteer = require('puppeteer');puppeteer.launch({ headless: false }).then(async browser => {const page = await browser.newPage();await page.tracing.start({ path: './trace.json' });await page.goto('<https://www.google.com>');await page.tracing.stop();await browser.close();
});
以上代码片段会将录制的 tracing 数据存储中 trace.json 中。
(是否记得之前 Tracing 协议的定义,与 start 配对的是 end,pupputeer 做了调整,具体在下面源码中体现)
再看看这个构建函数源码,其实非常简单:
// Page.ts
export class Page extends EventEmitter {constructor(client,...) {super()...this.#tracing = new Tracing(client);}get tracing(): Tracing {return this.#tracing;}
}
Tracing 是一个被标记为 Internal 的构造函数,意味着我们不能直接调用或扩展它的子类,如上代码片段,它挂载在 Page 上,随 Page 被实例化。
// Tracing.ts
import {assert} from './assert.js';
import {getReadableAsBuffer,getReadableFromProtocolStream,isErrorLike,
} from './util.js';
import {CDPSession} from './Connection.js';/*** @public*/
export interface TracingOptions {path?: string;screenshots?: boolean;categories?: string[];
}/*** The Tracing class exposes the tracing audit interface.* @remarks* You can use `tracing.start` and `tracing.stop` to create a trace file* which can be opened in Chrome DevTools or {@link <https://chromedevtools.github.io/timeline-viewer/> | timeline viewer}.** @example* ```ts* await page.tracing.start({path: 'trace.json'});* await page.goto('<https://www.google.com>');* await page.tracing.stop();* ```** @public*/
export class Tracing {#client: CDPSession;#recording = false;#path?: string;/*** @internal*/constructor(client: CDPSession) {this.#client = client;}/*** Starts a trace for the current page.* @remarks* Only one trace can be active at a time per browser.** @param options - Optional `TracingOptions`.*/async start(options: TracingOptions = {}): Promise<void> {assert(!this.#recording,'Cannot start recording trace while already recording trace.');const defaultCategories = ['-*','devtools.timeline','v8.execute','disabled-by-default-devtools.timeline','disabled-by-default-devtools.timeline.frame','toplevel','blink.console','blink.user_timing','latencyInfo','disabled-by-default-devtools.timeline.stack','disabled-by-default-v8.cpu_profiler',];const {path, screenshots = false, categories = defaultCategories} = options;if (screenshots) {categories.push('disabled-by-default-devtools.screenshot');}const excludedCategories = categories.filter(cat => {return cat.startsWith('-');}).map(cat => {return cat.slice(1);});const includedCategories = categories.filter(cat => {return !cat.startsWith('-');});this.#path = path;this.#recording = true;await this.#client.send('Tracing.start', {transferMode: 'ReturnAsStream',traceConfig: {excludedCategories,includedCategories,},});}/*** Stops a trace started with the `start` method.* @returns Promise which resolves to buffer with trace data.*/async stop(): Promise<Buffer | undefined> {let resolve: (value: Buffer | undefined) => void;let reject: (err: Error) => void;const contentPromise = new Promise<Buffer | undefined>((x, y) => {resolve = x;reject = y;});this.#client.once('Tracing.tracingComplete', async event => {try {const readable = await getReadableFromProtocolStream(this.#client,event.stream);const buffer = await getReadableAsBuffer(readable, this.#path);resolve(buffer ?? undefined);} catch (error) {if (isErrorLike(error)) {reject(error);} else {reject(new Error(`Unknown error:${error}`));}}});await this.#client.send('Tracing.end');this.#recording = false;return contentPromise;}
}
注意到 Puppeteer 提供的对 Tracing 的 config 有限,仅可自定义:
export interface TracingOptions {path?: string;screenshots?: boolean;categories?: string[];
}
看到以上代码片段,你可能会有一些疑惑:
- 为什么继承自 EventEmitter ?
- client 是什么?client.send 又是?
分享一下我的见解:
- Puppeteer 作为一个 Node 库,它需要事件模型支撑(可以在源码中看到大量的
on
emit
)来更好的串联各个模块,并实现解耦。而在Nodejs 中,事件模型就是我们常见的订阅发布模式,所有可能触发事件的对象都应该是一个继承自 EventEmitter 类的子类实例对象。 - client 即 CDPSession ,用于处理原生的 Chrome Devtools 协议通讯,而 client.send 即表示调用协议方法。
其实在 puppeteer 实现中,client 都承担着使用 CDP 与 server 通讯的责任,它其实就是 puppeteer launch 阶段与 server 通讯的 websocket transport。
// BrowserRunner.ts
async setupConnection(options: {...const transport = await WebSocketTransport.create(browserWSEndpoint);this.connection = new Connection(browserWSEndpoint, transport, slowMo);...return this.connection;}
}
4.3.2 chrome-remote-interface
setup
以远程调试模式启动 Chrome (增加参数—remote-debugging-port=9222
),DevTools server 将监听本地的端口9222
。
# 退出 Chorme 后再命令行输入命令,打开新的 Chrome
open -a "Google Chrome" --args --remote-debugging-port=9222
访问 http://localhost:9222/json 可以看到可用调试页面数据信息(包括打开的 Tab 页和 Chrome 上添加的 Extensions):
访问 http://localhost:9222/ + 任意一个 Tab 的 devtoolsFrontendUrl
,将会打开对该页面调试页。
或者同移动端调试一般,打开about://inspect
界面,可以发现此时本地浏览器被作为一个 remote device 来调试,找到具体 Tab 页点击 inspect 即可。
use case
如下片段,我们可以通过 CRI 使用 CDP 的所有 API。
const fs = require('fs');
const CDP = require('chrome-remote-interface');CDP(async (client) => {try {const {Page, Tracing} = client;// enable Page domain eventsawait Page.enable();// trace a page loadconst events = [];Tracing.dataCollected(({value}) => {events.push(...value);});await Tracing.start();await Page.navigate({url: '<https://github.com>'});await Page.loadEventFired();await Tracing.end();await Tracing.tracingComplete();// save the tracing datafs.writeFileSync('./trace.json', JSON.stringify(events));} catch (err) {console.error(err);} finally {await client.close();}
}).on('error', (err) => {console.error(err);
});
4.4 数据处理
对于以上收集到的 Tracing 数据(存储在 trace.json),因为数据量大而且晦涩,一般直接在 Chrome DevTools 或其他 timeline viewer 打开,用来分析 Web 站点性能表现的文件。
或者可以参照 Trace Event Format,使用脚本过滤出期望格式的 event 数据再做进一步分析。
参考
DevTools 实现原理与性能分析实战
Chrome DevTools Protocol 协议详解
朋友们,一起学习下 Chrome DevTools Protocol。相关推荐
- 让电驴(Emule)获取更高下载速度的方法 !(电驴下载慢的朋友可以进来学习下)
原文地址::http://bbs.guitarchina.com/thread-305056-1-1.html 很多朋友抱怨电驴下载速度太慢(我也在其中),所以小弟昨天特别去找了很多文章来研究后设置了 ...
- 学习Chrome Devtools 调试
前言(共分2部分内容) 常用命令和调试 黑盒脚本:Blackbox Script 控制台内置指令 远程调试WebView 1. Chrome Devtools 的用处 前端开发:开发预览.远程调试.性 ...
- Chrome DevTools 实现原理与性能分析实战
点击上方 前端Q,关注公众号 回复加群,加入前端Q技术交流群 作者:vivo 互联网浏览器内核团队-Li Qingmei 一.引言 从 2008 年 Google 释放出第一版的 Chrome 后,整 ...
- webapp网页调试工具Chrome Devtools
webapp网页调试工具Chrome Devtools 前言 css3说太多了,会显得文章显得千篇一律:介绍,介绍~demo,完结,然后就没有然后了.所以时不时插一篇扯蛋的玩意,起码还可以调节一下胃口 ...
- 玩转 Chrome DevTools,定制自己的调试工具
Chrome DevTools 是我们每天都用的工具,它可以查看元素.网络请求.断点调试 JS.分析性能问题等,是辅助开发的利器. 今天不讲怎么使用它,而是讲一个好玩的方向:定制自己的调试工具. 之前 ...
- Chrome DevTools
2019独角兽企业重金招聘Python工程师标准>>> Chrome DevTools Protocol Chrome开发者工具中文文档 Chrome DevTools Protoc ...
- Chrome DevTools 通过 cdp 调节 CPU Throttling
打开DevTools (快捷键ctrl + shift + i) 在DevTools 界面再打开DevTools let Main = await import('./devtools-fronten ...
- 你不知道的 Chrome DevTools 玩法
大家好,我是若川.今天再分享一篇 chrome devtools 的文章.之前分享过多篇. Chrome DevTools 全攻略!助力高效开发 前端容易忽略的 debugger 调试技巧 笔者 ...
- Java的学习(下)
JAVA的学习(下) 2019版尚硅谷Java入门视频教程,哔哩哔哩链接:https://www.bilibili.com/video/BV1Kb411W75N?p=5 十一.Java集合 11-1 ...
最新文章
- 一个不错的机器视觉库 SimpleCV: a kinder, gentler machine vision library
- strcpy 通过指针复制字符串出错问题
- B14_NumPy算术函数( add(),subtract(),multiply() 和 divide()、reciprocal()、power()、mod())
- devexpress能开发出html,DevExpress推出HTML5 JavaScript控件集
- lightoj1060_组合数学
- Docker最全教程之使用Docker搭建Java开发环境(十八)
- vc6.0 绘制散点图_vc有关散点图的一切
- 合并数据 - 方法总结(concat、append、merge、join、combine_first)- Python代码
- 10976 - Fractions Again?!
- ndk学习20: jni之OnLoad动态注册函数
- php multi_query()函数 实现批量执行sql语句
- ASP.NET MVC多语言 仿微软网站效果(转)
- SpringMVC的JSP页面中EL表达式不起作用${}
- win 7 64 安装 tensorflow
- JNI系列(2):jstring操作
- 【最优化基础】惩罚和障碍函数
- IPv6技术精要--第5章 IPv6公网单播地址
- C语言 求5分2分1分硬币
- MapGIS名词解释
- 【Pigeon源码阅读】高可用之熔断降级实现原理(十四)
热门文章
- Intellij Idea的日常
- 广州大学教育管理教育硕士专业学位研究生培养方案(2017非全日制)
- 我的世界java版记分板_我的世界计分板怎么用 我的世界计分板指令一览
- 任何实践都是理论的载体和表现形式(转)
- 推荐!关于学习数据科学的10件事
- 2014 android 机型排行,2014年10月十佳Android系统智能安卓手机排行榜单 Note 4第一名...
- ListPreference
- 心情低落的时候看看——自我疗伤
- The Rotation Game hdu-1667
- MAYA MAL查看窗体结构