devtools 面板扩展
本教程共 56 篇 · 第 32 篇 · 更新于 2026-08-13 · 约 8 分钟阅读
本节目标:学完你能给 Chrome 开发者工具加一个自己的面板或侧边窗格,知道 devtools 页面能用哪些 API、不能用哪些,也会用 inspectedWindow 读被检查页面的数据。
按 F12 打开的开发者工具,那一排标签页——Elements、Console、Network——是可以加东西的。React DevTools、Vue DevTools 都是这么做的:它们在开发者工具里插了一个属于自己的面板。
这一节讲这件事怎么实现。它是模块七里最”偏”的一个界面组件,用得不多,但一旦要做前端调试类工具,就绕不开。
32-1 devtools 扩展的结构
先建立一个整体印象。devtools 扩展比其他界面组件多一层:
- devtools 页面:一个隐藏的 HTML 页面,随开发者工具一起打开、一起关闭。它自己不显示任何界面,唯一任务是”注册面板”。
- 面板页面:你真正给用户看的界面,显示为开发者工具里的一个标签页。
- 被检查页面:用户正在调试的那个网页。
- 服务工作者(Service Worker):后台,负责跨上下文中转数据。
关系可以这样理解:devtools 页面是注册员,面板页面是门面,被检查页面是数据源,服务工作者是快递员。
Notedevtools 页面的生命周期跟着开发者工具走。用户打开 DevTools 它才加载,关掉 DevTools 它就销毁。而且每打开一个 DevTools 窗口,就有一份独立的 devtools 页面实例。
32-2 清单声明:devtools_page
声明只有一个字段,简单到不像话:
{
"manifest_version": 3,
"name": "我的 DevTools 面板",
"version": "1.0.0",
"devtools_page": "devtools.html"
}
devtools_page 指向那个隐藏的注册页。注意它是清单顶层字段,不是对象,值就是一个 HTML 路径。
Warning
devtools_page只能指向扩展包内的 HTML 文件,不能是远程地址。这条和整个 Manifest V3 的”禁止远程代码”规则一致。
32-3 devtools.html 里什么都不用写
这个页面不显示,所以别费劲写界面,只需要把脚本引进来:
<!DOCTYPE html>
<html>
<body>
<script src="devtools.js"></script>
</body>
</html>
真正的逻辑全在 devtools.js 里。这里有个旧说法要纠正:以前资料认为 devtools 页面只能碰 chrome.devtools.* 和消息 API,其实 DevTools 页面可以直接访问扩展 API——chrome.tabs.query()、chrome.storage.local.get()、chrome.scripting.executeScript() 在 devtools.js 里都能直接调;chrome.devtools.* 全家则只有 DevTools 窗口内的页面能用。
不过把重活发给服务工作者代办依然是常见做法——尤其当你需要感知开发者工具的开关状态时,长连接天然合适。后面 32-7 会给完整写法。
32-4 创建面板:devtools.panels.create
加一个新标签页,就调 chrome.devtools.panels.create():
chrome.devtools.panels.create(
'My Panel',
'MyPanelIcon.png',
'Panel.html',
function (panel) {
// 面板创建完成后执行的代码
}
);
四个参数依次是:
- 面板标题:显示在开发者工具标签栏上的文字,尽量短,两三个字最好。
- 图标路径:面板图标,扩展包内的相对路径。
- 页面路径:面板内容的 HTML 文件。
- 回调函数:创建成功后触发,参数
panel是这个面板的引用对象。
实际例子长这样:
chrome.devtools.panels.create('demo panel', 'icon.png', 'panel.html', () => {
console.log('用户切到了这个面板');
});
panel.html 就是一个普通网页,写你想要的表格、按钮、图表都行。它同样受扩展 CSP 约束:不能内联脚本,不能远程加载 JS。
面板对象有两个有用的事件:
chrome.devtools.panels.create('My Panel', 'icon.png', 'panel.html', (panel) => {
panel.onShown.addListener((panelWindow) => {
// 面板被切到前台,panelWindow 就是面板页面的 window
panelWindow.postMessage({ type: 'panel-shown' }, '*');
});
panel.onHidden.addListener(() => {
// 面板被切走了,可以暂停轮询之类的开销
});
});
onShown 的回调参数是面板页面的真实 window 对象,你可以直接调它上面的方法或 postMessage 通信。
Tip面板首次被用户点开之前,它的 HTML 不会真正加载。所以
onShown第一次触发的时机,才是面板页面可用的起点。别在create()的回调里就急着操作面板 DOM。
32-5 给 Elements 面板加侧边窗格
除了独立面板,还能在 Elements 面板右侧加一个自己的窗格,显示当前选中元素的附加信息。这就是 createSidebarPane():
chrome.devtools.panels.elements.createSidebarPane('My Sidebar', function (sidebar) {
// 侧边窗格初始化代码
sidebar.setObject({ some_data: 'Some data to show' });
});
setObject() 直接把一个 JS 对象渲染成可展开的树形结构,非常适合展示调试数据。
更实用的做法是:跟着用户选中的元素变化,实时刷新窗格内容。官方示例用它做了个”显示选中元素上的 jQuery 数据”的窗格:
// 这个函数会在被检查页面的上下文里执行
/* global $0 */
const page_getProperties = function () {
let data = window.jQuery && $0 ? jQuery.data($0) : {};
let props = Object.getOwnPropertyNames(data);
let copy = { __proto__: null };
for (let i = 0; i < props.length; ++i) copy[props[i]] = data[props[i]];
return copy;
};
chrome.devtools.panels.elements.createSidebarPane(
'jQuery Properties',
function (sidebar) {
function updateElementProperties() {
sidebar.setExpression('(' + page_getProperties.toString() + ')()');
}
updateElementProperties();
chrome.devtools.panels.elements.onSelectionChanged.addListener(
updateElementProperties
);
}
);
三个关键点:
$0是开发者工具的内置变量,代表当前在 Elements 里选中的那个元素。sidebar.setExpression()把一段表达式字符串扔到被检查页面里求值,再把结果显示在窗格里。elements.onSelectionChanged在用户点选别的元素时触发,用它重新求值就实现了实时刷新。
除了 setObject 和 setExpression,还有 setPage(),可以直接把一个 HTML 页面塞进窗格,完全自定义界面。
32-6 和被检查页面打交道
devtools 扩展的价值全在”读被检查页面的数据”。有两条路。
路子一:inspectedWindow.eval 直接求值
chrome.devtools.inspectedWindow.eval(
"inspect($$('head script')[0])",
function (result, isException) {}
);
eval() 把字符串当代码在被检查页面里跑,回调拿到结果。它能用开发者工具的便利变量,比如 $0(选中元素)、$$()(querySelectorAll 简写)、inspect()(在面板里定位对象)。
它很方便,但有硬限制:跑在页面自己的 JS 环境里,拿不到内容脚本的变量。想访问内容脚本上下文,得加参数:
chrome.devtools.inspectedWindow.eval('setSelectedElement($0)', {
useContentScriptContext: true
});
对应内容脚本里得有这个函数:
function setSelectedElement(el) {
// 拿到选中的元素,做你的处理
}
Warning
eval()是把字符串当代码执行,页面上的脚本可能篡改环境骗你。别用它做任何安全相关判断,也别把它的返回值当可信数据直接拼进 DOM。
路子二:注入内容脚本
需要更完整的能力时,从 devtools 页面注入一个内容脚本,用 inspectedWindow.tabId 指明目标:
// devtools.js
chrome.scripting.executeScript({
target: {
tabId: chrome.devtools.inspectedWindow.tabId
},
files: ['content_script.js']
});
chrome.devtools.inspectedWindow.tabId 就是被检查页面的标签页 id,这是 devtools 页面最重要的一个属性,几乎所有跨上下文操作都要用到它。
注入之后,内容脚本可以正常用 chrome.runtime.sendMessage() 把数据发出来。如果数据是从页面自身脚本发起的,还得走一次 window.postMessage 中转:
// injected-script.js(跑在页面里)
window.postMessage(
{
greeting: 'hello there!',
source: 'my-devtools-extension'
},
'*'
);
// content-script.js
window.addEventListener('message', function (event) {
// 只接受来自同一个 frame 的消息
if (event.source !== window) {
return;
}
var message = event.data;
// 只接受我们自己的消息。注意这不是万全之策,页面可以伪造。
if (
typeof message !== 'object' ||
message === null ||
message.source !== 'my-devtools-extension'
) {
return;
}
chrome.runtime.sendMessage(message);
});
那两个校验必须写。window.postMessage 是广播式的,页面上任何脚本都能发消息,不加过滤等于给页面开了后门。
32-7 和后台服务工作者通信
devtools 页面能直接调用扩展 API,但”缺什么找后台”依然是常见分工——尤其长连接能顺便检测开发者工具的开关状态。devtools 页面这边发起连接:
// devtools.js
const port = chrome.runtime.connect({ name: 'devtools-page' });
port.onMessage.addListener((message) => {
console.log('后台回话了:', message);
});
port.postMessage({ type: 'get-data', tabId: chrome.devtools.inspectedWindow.tabId });
后台这边接住,并顺手统计 DevTools 打开了几个:
// service-worker.js
let openCount = 0;
chrome.runtime.onConnect.addListener((port) => {
if (port.name !== 'devtools-page') return;
openCount++;
console.log('DevTools 打开了,当前实例数:', openCount);
port.onMessage.addListener((message) => {
if (message.type === 'get-data') {
chrome.tabs.get(message.tabId).then((tab) => {
port.postMessage({ type: 'data', title: tab.title });
});
}
});
port.onDisconnect.addListener(() => {
openCount--;
if (openCount === 0) {
console.log('最后一个 DevTools 窗口关掉了');
}
});
});
这个模式一举两得:既把活儿集中到了后台,又通过 onConnect / onDisconnect 天然知道了开发者工具的开关时机。
Note上例里后台用
chrome.tabs.get读tab.title,这需要清单里声明"tabs"权限,否则拿到的 title 是 undefined;只传tabId做转发则不需要。
Note别用
setInterval给这个连接发心跳来”保活”后台。Manifest V3 的服务工作者就该被事件唤醒、闲时休眠,强行保活是反模式。Port 断了下次有事件重新连就行。
32-8 调试与小结
调 devtools 扩展本身有点绕:你要调试的东西就在开发者工具里。方法是——对开发者工具再开一个开发者工具。把 DevTools 窗口切成独立窗口,然后在它上面按一次 Ctrl+Shift+I,就能看到 devtools 页面和面板页面的控制台。
改完代码后记得三步走:在 chrome://extensions 点重新加载扩展,关掉再重开 DevTools,最后刷新被检查页面。少一步就可能看到旧代码,白排查半天。
这一节的要点收一下:清单里写 devtools_page → devtools.html 只引一个脚本 → 用 devtools.panels.create() 加面板、elements.createSidebarPane() 加窗格 → 用 inspectedWindow.eval() 或注入内容脚本读页面数据 → API 不够就通过 Port 找服务工作者代办。
下一节讲快捷键 commands,把模块七的界面组件收个尾——让用户不用鼠标,一个组合键就能触发你的功能。