首页 / 浏览器扩展开发入门教程 / popup 弹出页开发

浏览器扩展开发入门教程

popup 弹出页开发

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

popupaction弹出页工具栏按钮Service Worker 通信内容脚本

本节目标:学完能给自己扩展加一个点击工具栏图标就弹出的小页面,并让它和后台、网页正常通信。

26-1 什么是 popup 弹出页

popup 是你点工具栏图标时弹出的一个小窗口。它本质上是一张扩展自带的 HTML 页面。

用户在网页上浏览内容时,工具栏图标固定在浏览器右上角。点一下,popup 就浮在图标下方。

它最适合承载轻量交互:一个开关、一次快捷搜索、一段状态提示、一个简单的表单。

popup 和普通网页非常像,可以写 HTML、CSS、JavaScript。但它运行在扩展上下文里,能调用 chrome.* API。

Note

popup 不是独立窗口,也不是标签页,依附于工具栏按钮存在。图标关掉它就被销毁。

很多人第一次接触扩展界面,就是从 popup 开始的。它离用户最近,也最容易上手。

和后台服务工作者(Service Worker)比,popup 更“看得见”;和内容脚本比,popup 又不直接贴在网页上。它是介于两者之间的轻界面。

这里有个判断标准:如果你的功能只需要用户点一下就办完,或者只是看一眼状态,popup 正合适。要装很多配置,那就交给后面要讲的选项页。

26-2 在清单里声明 popup

popup 必须在清单里通过 action 字段声明,不能直接写个 html 就生效。

action.default_popup 指向你的弹出页文件,default_icon 是图标,default_title 是悬停提示。

{
  "manifest_version": 3,
  "name": "我的扩展",
  "version": "1.0.0",
  "action": {
    "default_popup": "popup/popup.html",
    "default_icon": {
      "16": "icons/icon-16.png",
      "48": "icons/icon-48.png"
    },
    "default_title": "点击打开弹出页"
  }
}

目录约定一般把弹出页放进 popup 文件夹,和图标、脚本分开管理,结构更清楚。

default_popup 的值是相对扩展根目录的路径,写错路径会导致点击后白屏或打不开。

Tip

如果只设了 default_icon 没设 default_popup,点击图标会触发 action 的点击事件,而不会弹出页面。两者用途不同。

值得注意,MV3 里的工具栏入口只有 action 一个字段。老式 page action 在 MV3 中已不可用,所以你不用纠结该用哪个,统一写 action 即可。

26-3 popup 的 HTML 结构与脚本

弹出页就是一张普通的 HTML。下面是一份最小可运行的结构。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <link rel="stylesheet" href="popup.css">
  <title>我的弹出页</title>
</head>
<body>
  <button id="btn">执行操作</button>
  <script src="popup.js"></script>
</body>
</html>

脚本必须用外链文件引入,不能把可执行代码直接写在标签内容里。

document.getElementById("btn").addEventListener("click", () => {
  console.log("按钮被点击");
});

这里有个坑要注意:MV3 对扩展页面有严格的内容安全策略。内联脚本、内联事件属性都不可用。

下面这种写法会直接失效,千万别用:

<!-- 错误示范:内联脚本与内联事件会被 CSP 拦截 -->
<button onclick="doSomething()">执行</button>
<script>doSomething();</script>

正确做法永远是外链脚本文件,再用 addEventListener 绑定事件。

Note

CSP 限制是为了安全:它防止第三方脚本注入到你的扩展页面里执行,是 MV3 的重要防线。

popup 里也能用现代语法,比如模块化的 import。只要用 <script type="module" src="popup.js"> 引入,浏览器会按 ES Module 处理。这对后面用框架、拆分代码很有帮助。

26-4 popup 的样式写法

popup 的样式就是普通 CSS,写在 popup.css 里再外链引入即可。

body {
  width: 280px;
  margin: 0;
  font-family: sans-serif;
  padding: 12px;
}
button {
  width: 100%;
  padding: 8px;
}

popup 的尺寸会随内容自适应,但浏览器给了上限。宽度一般建议控制在 300 像素左右。

高度也不要过高,否则会超出屏幕可视范围。浏览器会自动给一个合理的弹出区域。

Note

popup 的字体、颜色都按你自己写的 CSS 走,不会继承网页样式。它是独立的小页面。

如果你希望界面和浏览器原生风格接近,可以自己加一套简洁的配色,不必引入额外框架。

要注意的是,popup 默认不加载任何浏览器自带样式,所以表单控件、按钮的长相完全由你的 CSS 决定。想要统一观感,不妨写一份基础的重置样式。

26-5 popup 的生命周期

popup 是按需加载的:你点开它才运行,关掉它就销毁。

这意味着每次打开,脚本都从零开始执行,之前的 DOM 状态不会保留。

如果你需要记住用户上次的输入,不能靠普通变量,得用 chrome.storage 存起来。

Tip

别在 popup 里放全局状态指望它一直在。popup 关闭后,内存里的东西全清空。

后台服务工作者也是非持久的,两者都不能当成常驻内存来用。持久数据一律走存储 API。

这也解释了为什么弹出页里的“临时变量”不能跨次使用,而 chrome.storage 可以。

实际开发中,一个常见做法是:popup 打开时先去 storage 读上次的值,回填到输入框;用户改动后再写回 storage。这样每次打开都像是“接着上次用”,体验是连贯的。

26-6 popup 与后台服务工作者通信

popup 经常要找后台要数据或下指令,靠一次性的消息通信最干净。

popup 这边发消息:

const response = await chrome.runtime.sendMessage({ type: "getCount" });
console.log(response.count);

后台服务工作者这边监听并回复:

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.type === "getCount") {
    sendResponse({ count: 3 });
  }
});

MV3 里 onMessage 支持返回一个 Promise,异步取数也能在 popup 里直接 await。

Note

MV3 里请统一用消息通信或存储 API 来跨上下文共享数据,不要依赖任何全局共享对象。

这里有个细节:popup 和后台是两个上下文,谁也看不到谁的内存。popup 能发消息,是因为 chrome.runtime 这个 API 是浏览器提供的“公共通道”,而不是因为它们在同一个进程。

如果后台当时正好处于休眠状态,发消息的动作会把它唤醒,然后在 onMessage 里处理。所以你不用操心“后台在不在”,浏览器会替你把它叫起来。

26-7 popup 控制内容脚本

想让 popup 去改当前网页,得先拿到当前标签页,再把消息转给内容脚本。

popup 里这样发:

const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
await chrome.tabs.sendMessage(tab.id, { action: "highlight" });

内容脚本这边接收:

chrome.runtime.onMessage.addListener((msg) => {
  if (msg.action === "highlight") {
    document.body.style.outline = "2px solid red";
  }
});

这条链路是 popup → 内容脚本,中间如果需要后台转发,就再加一层消息即可。

清单里要声明 content_scripts,并申请 activeTab 这类权限,否则消息发不出去。

{
  "permissions": ["activeTab"],
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content.js"]
    }
  ]
}
Tip

popup 拿当前页用 chrome.tabs.query 是最常见做法。activeTab 权限会在用户主动操作时自动授予。

有个常见坑:内容脚本没注入到当前页面时,sendMessage 会报错 “Receiving end does not exist”。稳妥的做法是先判断,或捕获异常,避免 popup 直接崩掉。

26-8 调试与常见坑

调试 popup 很简单:先点开它,再在弹出页里右键选择“检查”,就能打开 DevTools。

弹出页的脚本、网络、控制台都在这个独立面板里看,和网页调试一模一样。

最常见的几个坑:一是忘了外链脚本写成内联,被 CSP 拦掉;二是以为 popup 能记住变量;三是消息没回导致 await 一直挂起。

Tip

消息监听里如果要用 sendResponse 异步回复,记得返回 true 或返回一个 Promise,否则响应会丢失。

popup 是扩展里最常用也最友好的界面。把它和消息、存储配合好,基础交互就齐了。

最后一个提醒:改了弹出页的 HTML、CSS 或 JS,关掉再重新点开弹出页就是最新内容,不用刷新扩展;只有改了 manifest.json 或后台脚本这类,才需要回扩展管理页点“重新加载”。

26-9 小结

popup 看起来简单,却是新手最容易栽跟头的地方。总结三件事:声明靠 action.default_popup,脚本必须外链,状态不要存内存。

它和后台、内容脚本的通信都走 chrome.* 的消息机制,配合 chrome.storage 才能把数据留下来。

当你需要更复杂的界面时,再考虑选项页或独立页面,popup 保持轻量最舒服。

Tip

把 popup 当成一个“一次性小工具”:打开即用、用完即走,不要试图在里面维护复杂状态。