1. Chrome DevTools Extension
熟悉React的同学,可能对React Developer Tools并不陌生,
刚看到的时候,我也觉得很神奇,
因为React Developer Tools和其他Chrome Extension不同,
它居然出现在了Chrome开发者工具栏中,和原生的DevTools一样强大。
例如,可以审查元素,查看元素的属性,等等。
后来才知道,像这种出现在Chrome开发者工具栏中的扩展,称为Chrome DevTools Extension。
比起普通的Chrome Extension,Chrome DevTools Extension可以访问更多API,例如,
(1)devtools.inspectedWindow
(2)devtools.network
(3)devtools.panels
其中包括了,与当前审查窗口相关的,与网络请求相关的,以及与开发者工具栏相关的API。
2. 背景 & 基本概念
在某一具体项目中,有一个这样的需求,
我们需要选择页面中发起的http请求,然后将它们保存到数据库中。
由于页面发起的请求可能会发往不同的服务器,所以在服务器端解决这个问题就显得比较麻烦,
而编写一个Chrome DevTools Extension会更简单直接。
下文我将这个功能的核心抽离出来,作为一个例子,来还原Chrome DevTools Extension的编写方法。
为此,我们先熟悉几个基本的概念。
(1)tab页
Chrome浏览器是由tab页组成的,一个浏览器实例中可以打开多个tab页。
(2)DevTools Window
每个tab页,都可以打开自己的开发者工具窗口,称为DevTools Window。
注意,每个tab页都有自己独立的DevTools Window,
只是切换tab页的时候,只会显示当前tab页的DevTools Window。
(3)DevTools Page 和 Panel
下面我们来创建一个Chrome DevTools Extension项目,目录结构如下,
chrome-devtools-extension-example
├── devtools.html // DevTool Page
├── devtools.js // DevTool Page中引用的js
├── manifest.json // 入口
├── panel.html // 开发者工具栏选项卡页面
└── panel.js // 选项卡页面中引用的js
其中manifest.json
是入口,它会声明一个对用户不可见的DevTools Page。
在本例中为devtools.html
,
{
"name": "PageRecorder",
"version": "1.1.0",
"minimum_chrome_version": "10.0",
"description": "Record all http requests in a page.",
"devtools_page": "devtools.html",
"manifest_version": 2
}
DevTools Page引入的js,具有访问DevTools API的能力,
包括上文提到的那些API,devtools.inspectedWindow,devtools.network,devtools.panels。
DevTools Page对用户是不可见的,如果需要在开发者工具栏中创建新的DevTool选项卡,
还需要在DevTools Page使用一下方法来创建,DevTool选项卡,官方称为Panel。
原生的Panel包括,Elements,Console,Network,等等。
// 创建一个Panel
chrome.devtools.panels.create(
// title
'ChromeDevToolsExtensionExample',
// iconPath
null,
// pagePath
'panel.html'
);
以上,我们在DevTool Page中创建了一个新的Panel,名字为ChromeDevToolsExtensionExample
。
其中,panel.html
,我们只是简单的写了一个Hello World!
。
值得注意的是,每个Panel都可以加载自己的html,js和css,且具有和DevTools Page一样的权限。
(4)Panel的生命周期
Panel只有在第一次被激活的时候,才进行实例化,
同一个DevTools Window中的不同Panel切换时,不会重新加载。
当前tab页刷新时,Panel也不会重新加载。
DevTools Window关闭后,Panel将被销毁。
因此,我们要想使用Chrome DevTools Extension,就必须先打开开发者工具窗口,
然后再激活我们新建的DevTools Panel。
3. 监听请求
上文我们提到了,Chrome DevTools Extension可以访问devtools.network API,
现在我们来展示使用chrome.devtools.network.onRequestFinished.addListener
来获取请求。
为此,我们新建了一个panel.js
文件,并在panel.html
中引用它。
// Chrome DevTools Extension中不能使用console.log
const log = (...args) => chrome.devtools.inspectedWindow.eval(`
console.log(...${JSON.stringify(args)});
`);
// 注册回调,每一个http请求响应后,都触发该回调
chrome.devtools.network.onRequestFinished.addListener(async (...args) => {
try {
const [{
// 请求的类型,查询参数,以及url
request: { method, queryString, url },
// 该方法可用于获取响应体
getContent,
}] = args;
log(method, queryString, url);
// 将callback转为await promise
// warn: content在getContent回调函数中,而不是getContent的返回值
const content = await new Promise((res, rej) => getContent(res));
log(content);
} catch (err) {
log(err.stack || err.toString());
}
});
以上就是panel.js
的完整内容了,我们还需要做以下几点说明,
(1)Chrome DevTools Extension中,不能直接使用console.log
,
所以本例中使用了devtools.inspectedWindow API中的chrome.devtools.inspectedWindow.eval
方法,
在当前审查的窗口中直接求值一段js代码,从而间接实现打印日志的功能。
(2)与获取http请求的method
,queryString
,url
不同的是,
我们需要调用getContent
方法来获取http响应体,
并且,getContent
是一个高阶的异步函数。
所谓高阶函数,指的是,它接受一个回调函数作为参数getContent(content=>{ })
,
这个回调函数的参数content
,才是对应http请求的响应体。
所谓异步,指的是,当回调函数还没触发的时候,getContent
就已经返回了。
这也导致了事件监听函数,也不得不具有异步性。
(3)由于事件监听函数是异步的,
所以,有可能在上一个onRequestFinished
事件还未处理完的情况下,
下一个onRequestFinished
的监听函数就又被触发了。
这就导致了,以上例子中,log(method, queryString, url);
和log(content);
,
可能是乱序打印的。
这个问题我们曾经讨论过,可参考:怎样按触发顺序执行异步任务。
总结
到此为止,我们最简版的Chrome DevTools Extension示例已经介绍完了,
以下是可以运行的源码地址:github: chrome-extension-example
(注:不是master
分支,而是simply
分支。
Chrome扩展遵循一种优秀的设计原则,
那就是在设计系统的时候,应该想办法让扩展对用户而言,与原生功能平权。
这种对称性,会拉近原生与扩展之间的距离,从而让系统架构从一开始就建立在灵活的基础之上。
参考
Chrome Extension
Extending DevTools
devtools.network#onRequestFinished
github: chrome-extension-example
简书:怎样按触发顺序执行异步任务