popup 弹出页开发
本教程共 56 篇 · 第 26 篇 · 更新于 2026-08-13 · 约 8 分钟阅读
本节目标:学完能给自己扩展加一个点击工具栏图标就弹出的小页面,并让它和后台、网页正常通信。
26-1 什么是 popup 弹出页
popup 是你点工具栏图标时弹出的一个小窗口。它本质上是一张扩展自带的 HTML 页面。
用户在网页上浏览内容时,工具栏图标固定在浏览器右上角。点一下,popup 就浮在图标下方。
它最适合承载轻量交互:一个开关、一次快捷搜索、一段状态提示、一个简单的表单。
popup 和普通网页非常像,可以写 HTML、CSS、JavaScript。但它运行在扩展上下文里,能调用 chrome.* API。
Notepopup 不是独立窗口,也不是标签页,依附于工具栏按钮存在。图标关掉它就被销毁。
很多人第一次接触扩展界面,就是从 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 绑定事件。
NoteCSP 限制是为了安全:它防止第三方脚本注入到你的扩展页面里执行,是 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 像素左右。
高度也不要过高,否则会超出屏幕可视范围。浏览器会自动给一个合理的弹出区域。
Notepopup 的字体、颜色都按你自己写的 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。
NoteMV3 里请统一用消息通信或存储 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"]
}
]
}
Tippopup 拿当前页用 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 当成一个“一次性小工具”:打开即用、用完即走,不要试图在里面维护复杂状态。