首页 / 浏览器扩展开发入门教程 / devtools 面板扩展

浏览器扩展开发入门教程

devtools 面板扩展

本教程共 56 篇 · 第 32 篇 · 更新于 2026-08-13 · 约 8 分钟阅读

devtoolsdevtools_pagepanelsinspectedWindow开发者工具Manifest V3

本节目标:学完你能给 Chrome 开发者工具加一个自己的面板或侧边窗格,知道 devtools 页面能用哪些 API、不能用哪些,也会用 inspectedWindow 读被检查页面的数据。

按 F12 打开的开发者工具,那一排标签页——Elements、Console、Network——是可以加东西的。React DevTools、Vue DevTools 都是这么做的:它们在开发者工具里插了一个属于自己的面板。

这一节讲这件事怎么实现。它是模块七里最”偏”的一个界面组件,用得不多,但一旦要做前端调试类工具,就绕不开。

32-1 devtools 扩展的结构

先建立一个整体印象。devtools 扩展比其他界面组件多一层:

  1. devtools 页面:一个隐藏的 HTML 页面,随开发者工具一起打开、一起关闭。它自己不显示任何界面,唯一任务是”注册面板”。
  2. 面板页面:你真正给用户看的界面,显示为开发者工具里的一个标签页。
  3. 被检查页面:用户正在调试的那个网页。
  4. 服务工作者(Service Worker):后台,负责跨上下文中转数据。

关系可以这样理解:devtools 页面是注册员,面板页面是门面,被检查页面是数据源,服务工作者是快递员。

Note

devtools 页面的生命周期跟着开发者工具走。用户打开 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 在用户点选别的元素时触发,用它重新求值就实现了实时刷新。

除了 setObjectsetExpression,还有 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.gettab.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,把模块七的界面组件收个尾——让用户不用鼠标,一个组合键就能触发你的功能。