开发工具页面 (Devtools Pages)
现代浏览器为开发者提供了一套丰富的开发工具,用于调试、性能分析和检查网页。浏览器扩展程序可以通过在开发工具界面中提供自定义界面来补充这些工具,并且能够访问特殊的开发工具 API。
开发工具页面的目标受众是网页开发者,因此面向消费者的扩展程序可能不需要使用它们。一般来说,浏览器扩展程序只有在希望使用开发工具 API 来检查或分析网页时才需要开发工具页面。
注意:本章中有两个名称相似的不同概念:“开发工具页面”(devtools pages)和“开发者工具”(developer tools)。“开发者工具”指的是通过右键单击网页并选择“检查”而打开的浏览器原生界面。“开发工具页面”则是指添加到开发者工具内部的额外界面。浏览器扩展程序使用开发工具 API 来添加这些额外界面。
开发工具页面简介
浏览器的开发者工具是一个特殊界面,可对活动网页实现完全控制和透明化(见图 10-1)。它能够全面检查和管理 HTML,添加和修改页面内容,并监控所有网络流量。它不受跨域规则的限制,因此能够完全检查来自任何来源的网页框架。其 JavaScript 调试器可以暂停执行,并展示 JavaScript 运行时的整个内存模型。
浏览器开发者工具的界面复杂且信息密集,因此它依赖于分层选项卡界面,将这些视图分解为独立的模块。浏览器扩展程序可以通过添加额外的选项卡来增强此开发者工具界面。这些添加的选项卡与其他由扩展程序控制的用户界面类似。
创建开发工具页面
浏览器扩展程序采用一种特殊策略,为浏览器的开发者工具添加额外界面。扩展程序可以在 manifest
文件中定义 devtools_page
属性。每次打开浏览器的开发者工具时,所引用的 HTML 页面都会渲染为一个无头页面(headless page)。此无头页面的唯一目的是加载并执行使用开发工具 API 插入额外界面的脚本。
名称可能会让人感到困惑,因此最好通过示例来学习。以下是一个简单的扩展程序示例,它提供了一个什么也不做的开发工具页面:
示例 10-1a. manifest.json
{
"name": "MVX",
"version": "0.0.1",
"manifest_version": 3,
"devtools_page": "devtools.html"
}
示例 10-1b. devtools.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<script src="devtools.js"></script>
</body>
</html>
示例 10-1c. devtools.js
// 使用此脚本访问开发工具 API
无需担心加载此扩展程序,因为它目前什么也不做。
注意:无头开发工具页面仅在每次打开开发者工具时运行。这意味着,即使扩展程序更新或页面重新加载,对添加的面板所做的更改也不会重新渲染。在对开发工具页面进行更新时,必须关闭并重新打开开发者工具界面,或重新加载已更新的开发工具框架。
添加面板和侧边栏
浏览器扩展程序可以向开发者工具添加两种类型的界面:面板和侧边栏。它们出现在开发者工具中的不同位置,但行为相同。面板和侧边栏都像内容脚本一样,可以访问 WebExtensions API 的有限子集。此外,它们还允许访问开发工具 API。
注意:大多数浏览器扩展程序界面(如弹出窗口和选项页面)是通过声明式方式创建的:清单指定一个 HTML 文件路径作为入口点,浏览器知道如何解释并自动整合新界面。相反,开发工具页面是通过命令式方式创建的:要创建界面,必须调用开发工具 API 中的方法并传入一个 HTML 文件路径。
添加面板
要向开发者工具添加新面板,请使用 chrome.devtools.panels.create()
方法。按如下方式修改之前的示例:
示例 10-2a. foo_panel.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<h1>我是 foo 面板!</h1>
</body>
</html>
示例 10-2b. devtools.js
chrome.devtools.panels.create("示例开发工具",
"",
"foo_panel.html");
create()
方法接受一个标题、一个图标 URL(此处留空)和一个面板 HTML 文件。加载此扩展程序后,关闭并重新打开开发者工具,然后查看面板菜单。您会注意到有一个名为“示例开发工具”的新选项(图 10-2)。
点击该选项即可显示新面板(图 10-3)。
添加侧边栏
侧边栏与面板略有不同。面板是开发者工具中的一个顶级界面,而侧边栏是嵌套在开发者工具中现有界面旁边的界面。目前,有两个界面可以添加侧边栏:
• Elements
• Sources
添加侧边栏的过程稍显复杂。需要针对目标界面(elements
或 sources
)调用其对应的 createSidebarPane()
方法。该方法的回调函数会接收到新的侧边栏面板对象作为参数,可以通过该对象的 setPage()
方法加载自定义页面。
以下示例修改了之前的扩展,改为添加两个侧边栏:一个添加到 Elements
界面,另一个添加到 Sources
界面:
示例 10-3a. devtools.js
chrome.devtools.panels.sources.createSidebarPane("Demo Sources Sidebar",
(sidebar) => {
sidebar.setPage("sources_sidebar.html");
});
chrome.devtools.panels.elements.createSidebarPane("Demo Elements Sidebar",
(sidebar) => {
sidebar.setPage("elements_sidebar.html");
});
示例 10-3b. elements_sidebar.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<h1>我是元素侧边栏!</h1>
</body>
</html>
示例 10-3c. sources_sidebar.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<h1>我是源代码侧边栏!</h1>
</body>
</html>
安装此扩展,并重新打开开发者工具。在 Elements
和 Sources
标签页中,您将看到一个新的子选项(图 10-4、10-5、10-6 和 10-7)。
点击这些选项以查看新的侧边栏界面:
当然,这些视图目前还没有实际功能。在下一节中,我们将介绍这些视图可以访问的特殊方法。
注意:每次打开标签页时,面板和侧边栏视图都会重新渲染 HTML 页面。例如,如果您点击自定义面板,然后切换到元素标签页,再切换回原始面板,面板将重新渲染两次。如果您需要保留状态,请牢记这一点。
Devtools API
Devtools API 是 WebExtensions API 的一个扩展,仅在开发者工具界面内的扩展视图中可用。该 API 包含以下四个命名空间:
• chrome.devtools.panels
用于以编程方式向开发者工具添加自定义面板和侧边栏。这些方法通常从开发者工具打开时渲染的无头开发者工具页面调用。
• chrome.devtools.network
用于嗅探当前页面的网络流量。它类似于 webNavigation API,但 devtools 版本以 HTTP 存档格式(HAR)记录流量。
• chrome.devtools.inspectedWindow
用于以普通 WebExtensions API 无法实现的方式检查当前网页。它可以在页面上下文中评估 JavaScript 表达式,并查看当前网页正在使用的资源列表(如文档、脚本或图像)。
• chrome.devtools.recorder
用于自定义开发者工具的 Recorder 面板。在编写本书时,此 API 处于预览阶段,仅支持导出记录器数据。
Google Chrome 和其他基于 Chromium 的浏览器将完全支持 Devtools API。其他浏览器(如 Safari 和 Firefox)可能不完全支持这些 API。
嗅探网络流量
大多数 Web 开发人员都非常熟悉开发者工具的网络面板。该面板显示有关活动网页的传出网络请求的极其详细的实时信息。浏览器扩展可以通过 Devtools API 访问相同的数据源。该 API 允许访问记录的网络请求日志,以及为某些网络请求生命周期事件添加处理程序。
API 将以 HTTP 存档(HAR)格式提供有关网络请求的信息。下面显示了向 wikipedia.org 发出 GET 请求的 HAR 格式的截断示例:
示例 HAR 数据(wikipedia.org GET 请求)
{
"_initiator": {
"type": "other"
},
"_priority": "VeryHigh",
"_resourceType": "document",
"cache": {},
"connection": "769999",
"pageref": "page_2",
"request": {
"method": "GET",
"url": "https://www.wikipedia.org/",
"httpVersion": "http/2.0",
"headers": [
{
"name": ":method",
"value": "GET"
}, {
"name": ":path",
"value": "/"
},
{
"name": ":scheme",
"value": "https"
},
... ],
"queryString": [],
"cookies": [
... ],
"headersSize": -1,
"bodySize": 0
},
"response": {
"status": 304,
"statusText": "",
"httpVersion": "http/2.0",
"headers": [
{
"name": "content-encoding",
"value": "gzip"
}, {
"name": "content-type",
"value": "text/html"
},
... ],
"cookies": [],
"content": {
"size": 75189,
"mimeType": "text/html"
},
"redirectURL": "",
"headersSize": -1,
"bodySize": 0,
"_transferSize": 1145,
"_error": null
},
"serverIPAddress": "208.80.154.224",
"startedDateTime": "2022-08-24T15:28:35.006Z",
"time": 45.61999998986721,
"timings": {
"blocked": 3.410000022381544,
"dns": -1,
"ssl": -1,
"connect": -1,
"send": 0.2370000000000001,
"wait": 41.07399999056011,
"receive": 0.8989999769255519,
"_blocked_queueing": 1.8070000223815441
}
}
对于在网络面板上花费大量时间的开发人员来说,您会很快识别出这是面板中显示的所有信息的 JSON 数据转储。
Devtools API 仅有一个方法和两个事件供您使用,但这足以让您能够设计自己的网络检查器版本。以下示例创建了一个非常简单的自定义网络面板:
示例 10-4a. manifest.json
{
"name": "MVX",
"version": "0.0.1",
"manifest_version": 3,
"devtools_page": "devtools.html"
}
示例 10-4b. devtools.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<script src="devtools.js"></script>
</body>
</html>
示例 10-4c. devtools.js
chrome.devtools.panels.create("Devtools Traffic", "", "traffic_panel.html");
示例 10-4d. traffic_panel.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="traffic_panel.js" defer></script>
</head>
<body></body>
</html>
示例 10-4e. traffic_panel.js
function logRequest(har) {
document.body.innerHTML += `
<div>
${har.request.method} ${har.request.url}
(${har.response.status})
</div>`;
}
// 一次性调用以获取迄今为止累积的所有请求。
// 这允许您在开发者工具面板之间导航,而不会丢失流量日志。
chrome.devtools.network.getHAR((harLog) => {
for (let har in harLog.entries) {
logRequest(har);
} });
// 每次顶级网页更改 URL 时触发
chrome.devtools.network.onNavigated.addListener((url) => {
document.body.innerHTML += `<hr><h1>${url}</h1><hr>`;
});
// 每次网页中的任何内容发出网络请求时触发
chrome.devtools.network.onRequestFinished.addListener(
(har) => logRequest(har));
加载此扩展,打开开发者工具,并选择新的“Devtools Traffic”面板。导航到任何网页,您将看到所有请求都输出到此面板中,如图 10-8 所示。
检查页面
DevTools API 允许您使用 eval()
方法在网页的上下文中运行 JavaScript 表达式。此方法与 scripting.executeScript()
非常相似:您在本地 DevTools 上下文中提供一段 JavaScript,它会被传递并在远程网页上下文中执行,并返回一个可序列化的值。然而,与 scripting.executeScript()
方法不同的是,eval()
还提供了对网页 JavaScript 状态的访问。此外,eval()
使用字符串表达式,而 executeScript
使用函数对象或文件引用。
在“内容脚本”章节中,我们探讨了内容脚本如何无法访问宿主网页的 JavaScript 状态,并举了一个例子说明注入的脚本无法看到宿主网页的全局 jQuery 对象。使用 DevTools API,现在可以查看和交互宿主网页的 JavaScript 状态。以下示例展示了这种行为:
示例 10-5a. manifest.json
{
"name": "MVX",
"version": "0.0.1",
"manifest_version": 3,
"devtools_page": "devtools.html"
}
示例 10-5b. devtools.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<script src="devtools.js"></script>
</body>
</html>
示例 10-5c. devtools.js
chrome.devtools.panels.create("Devtools Inspector", "", "inspect_panel.html");
示例 10-5d. inspect_panel.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="inspect_panel.js" defer></script>
</head>
<body>
<button id="check">CHECK FOR JQUERY</button>
</body>
</html>
示例 10-5e. inspect_panel.js
document.querySelector("#check").addEventListener("click", () => {
chrome.devtools.inspectedWindow.eval(
`({
'url': window.location.href,
'usesJquery': !!window.jQuery
})`,
null,
(result) => {
const div = document.createElement("div");
div.innerText = `${result.url} uses jQuery: ${result.usesJquery}`;
document.body.appendChild(div);
}
);
});
加载此扩展并在 jquery.com
(有全局 jQuery 对象)和 wikipedia.org
(没有)上打开 DevTools(图 10-9)。点击每个网站上的按钮,将准确地在 DevTools 面板中记录该网站是否使用 jQuery。
对于复杂的表达式,您可以使用自调用函数表达式(IIFE)来返回值。之前的表达式可以重构为使用 IIFE:
chrome.devtools.inspectedWindow.eval(
`(() => {
const url = window.location.href;
const usesJquery = !!window.jQuery;
return { url, usesJquery };
})()`,
... )
传递给 eval()
的 JavaScript 表达式也可以访问浏览器的控制台 API,因此可以使用特殊的控制台值和方法,如 $0
和 inspect()
。当在扩展元素(Elements)界面的侧边栏中使用时,这些功能尤其有用,因为可以直接与元素(Elements)面板的 DOM 树浏览器集成。下面的示例正是这样做的:
示例 10-6a. manifest.json
{
"name": "MVX",
"version": "0.0.1",
"manifest_version": 3,
"devtools_page": "devtools.html"
}
示例 10-6b. devtools.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<script src="devtools.js"></script>
</body>
</html>
示例 10-6c. devtools.js
chrome.devtools.panels.elements.createSidebarPane(
"Devtools Inspector",
(sidebar) => {
sidebar.setPage("inspect_panel.html");
}
);
示例 10-6d. inspect_panel.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="inspect_panel.js" defer></script>
</head>
<body>
<button id="inspect">INSPECT IMG</button>
<button id="tagname">ACTIVE TAG NAME</button>
</body>
</html>
示例 10-6e. inspect_panel.js
document.querySelector("#inspect").
addEventListener("click", () => {
chrome.devtools.inspectedWindow.eval(
`inspect(document.querySelector('img'))`
); });
document.querySelector("#tagname").addEventListener("click",
() => {
chrome.devtools.inspectedWindow.eval(
`$0?.tagName`, null,
(result) => {
const div = document.createElement("div");
div.innerText = result;
document.body.appendChild(div);
}); }
);
加载此扩展,打开开发者工具,选择“元素(Elements)”选项卡,并打开“Devtools Inspector”侧边栏。
在此示例中,“INSPECT IMG”按钮对文档中出现的第一个图像调用 inspect()
。因为这是从元素(Elements)面板内的侧边栏调用的,所以您会立即在原生浏览器的 DOM 检查器中看到此更改(图 10-10)。 点击 "ACTIVE TAG NAME" 按钮时,将使用 $0 令牌来引用 DOM 检查器中最后被检查的节点,并打印出该节点的标签名(图 10-11)。
这种与原生浏览器的双向集成使您能够构建复杂的开发者工具页面,这些页面可以快速引导用户在页面中进行操作。
内容脚本与开发者工具消息传递
通常,当开发者工具页面能够与注入到页面中的内容脚本协同工作时,它们才最为有用。每个组件都有其自身的职责:
- 开发者工具页面:负责与 DevTools API 交互。
- 内容脚本:负责管理页面覆盖层和类似的小部件。
- 两者可以通过消息传递进行协调。
然而,这种策略存在两个问题:
开发者工具页面无法直接向特定标签页发送消息:
- 尽管 DevTools API 可以通过
chrome.devtools.inspectedWindow.tabId
访问宿主页面的标签页 ID,但它无法使用chrome.tabs.sendMessage()
向该特定标签页发送消息。
- 尽管 DevTools API 可以通过
内容脚本向开发者工具发送消息时的问题:
- 当从内容脚本向开发者工具发送消息时,必须考虑同时打开多个开发者工具界面的情况。内容脚本没有直接的方法将消息仅发送到该页面的开发者工具面板。
有几种巧妙的策略可以解决这些问题:
使用
runtime.sendMessage()
:- 开发者工具页面可以向内容脚本发送消息,并包含标签页 ID。内容脚本可以根据页面的标签页 ID 进行过滤,并直接响应该消息,该消息仅会被原始的开发者工具页面看到。
- 为了建立伪双向通道,内容脚本可以排队消息,开发者工具页面可以间歇性地轮询。
开发者工具页面与后台页面建立长连接:
- 后台页面管理一个标签页 ID 到连接的映射,因此它可以将每个消息路由到正确的连接。
开发者工具页面结合注入脚本与内容脚本作为中介:
- 然后使用
window.postMessage()
将消息传递给内容脚本。
- 然后使用
提示:Google Chrome 团队对此有很好的说明,详情请参阅:https://developer.chrome.com/docs/extensions/mv3/devtools/#solutions。
其他 DevTools API 功能
除了本章中展示的示例外,DevTools API 还包含一些其他有趣的功能:
使用内容脚本上下文评估表达式:
- 开发者工具页面可以使用
useContentScriptContext
选项,以扩展内容脚本的上下文评估eval()
表达式。这对于与内容脚本交换数据以及在页面上下文中访问 WebExtensions API 非常有用。
- 开发者工具页面可以使用
检查页面正在使用的资源:
- 开发者工具页面可以通过
chrome.devtools.inspectedWindow.getResources()
检查页面当前正在使用的资源(文档、样式表、脚本、图像等),并通过onResourceAdded
和onResourceContentCommitted
事件监听这些资源的变化。
- 开发者工具页面可以通过
强制页面重新加载:
- 开发者工具页面可以使用
chrome.devtools.inspectedWindow.reload()
强制页面重新加载。此reload()
方法还允许您忽略浏览器缓存、在下次加载时注入自定义脚本以及伪造用户代理。
- 开发者工具页面可以使用
定义插件以导出记录器数据:
- 开发者工具页面可以使用
chrome.devtools.recorder.registerRecorderExtensionPlugin()
定义插件以导出记录器数据。有关记录器面板的更多信息,请参阅:https://developer.chrome.com/docs/devtools/recorder/
- 开发者工具页面可以使用
总结
在本章中,您学习了扩展如何扩展浏览器的原生开发者工具界面。我们探讨了扩展将用户界面注入到开发者工具中的不寻常方式,以及开发者工具页面独有的所有主要 DevTools API 功能。
在下一章中,我们将涵盖所有主要的 WebExtensions API 以及它们的使用方法。