Popup 与选项页

Chrome 插件开发指南 · action popup、options_page、React 集成

作者 David更新于

Popup、选项页、Side Panel 都属于扩展页面——跑在 chrome-extension:// 源下的普通 HTML,能用全部 chrome.* API,但受扩展 CSP 约束。

{ "action": { "default_popup": "popup.html" } }
<!-- popup.html -->
<!doctype html>
<html>
  <head><link rel="stylesheet" href="popup.css" /></head>
  <body>
    <div id="app"></div>
    <script src="popup.js" type="module"></script>
  </body>
</html>

三条必须记住的特性

① Popup 一关就彻底销毁。 不是隐藏——是整个文档被销毁,JS 状态、定时器、未完成的请求全没了。

所以:任何要保留的状态必须立刻写进 chrome.storage,别指望「用户再打开时还在」。

这也意味着别在 popup 里发起长任务。用户点完就关,请求还没回来 popup 就死了。长任务交给 Service Worker。

② 尺寸由内容撑开,不能用 JS 设置窗口大小。

body {
  width: 360px;      /* 上限 800px */
  min-height: 200px; /* 上限 600px */
}

不写宽度的话,Popup 会按内容自适应——异步加载数据时会出现「先窄后宽」的跳动。固定宽度是标准做法。

③ 没有 alert / confirm / prompt。 扩展页面里这些被禁用,要自己画对话框。

// popup.js
const data = await chrome.runtime.sendMessage({ type: 'GET_DATA' });
render(data);

// 也可以直接读 storage,往往更简单
const { config } = await chrome.storage.local.get('config');

优先用 chrome.storage 而不是消息。 popup 打开时读一次 storage,比来回发消息简单,还天然避开了「SW 正好被回收、消息发不出去」的时序问题。

只有需要触发动作(而非读数据)时才发消息。

选项页

两种写法:

{ "options_page": "options.html" }
{
  "options_ui": {
    "page": "options.html",
    "open_in_tab": false
  }
}
options_pageoptions_ui
打开方式新标签页嵌入 chrome://extensions 的对话框(open_in_tab: false)
适合设置项多、需要完整版面设置项少

推荐用 options_ui——它更现代,且能通过 open_in_tab: true 退化成新标签页行为。

从代码打开选项页:

chrome.runtime.openOptionsPage();

在 popup 里放一个「设置」入口指向它,是常见做法。

选项页的配置该存哪

// 用 sync:跨设备同步,但配额小(约 100 KB,单项 8 KB)
await chrome.storage.sync.set({ theme: 'dark', apiKey: '' });

用户配置几乎总该进 sync——换台电脑设置还在,体验好很多。但别把 API key 之类的东西放 sync(它会同步到 Google 账号)。配额与选型见存储 API。

完整实战见第 6 课:Options Page。

三种 UI 怎么选

Popup选项页Side Panel
触发点工具栏图标主动打开点图标 / API 打开
生命周期关闭即销毁标签页级可长期停留
与页面并存否(点页面就关)否是
适合快速操作、状态一览配置需要边看页面边操作

判据: 用户要不要「一边看网页一边用你的 UI」?要——Side Panel;不要——Popup。

React / Vue 集成

扩展页面就是普通网页,框架照常用。唯一的额外约束是 CSP 禁止 eval 和内联脚本。

Vite 配置

// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  build: {
    rollupOptions: {
      input: {
        popup: 'popup.html',
        options: 'options.html',
      },
    },
  },
});

多入口分别打包,manifest 里指向 dist/ 下的产物。

开发模式的 HMR 在扩展里跑不通。 Vite dev server 靠注入内联脚本和 eval 工作,两者都被扩展 CSP 挡住。

三条出路:

  1. build --watch —— 改完自动重建,去 chrome://extensions 点刷新。最朴素,但可靠。
  2. 用 WXT 或 Plasmo —— 它们专门解决了扩展的 HMR 问题。
  3. 先在普通网页环境里开发 UI(chrome.* 打桩),成型后再接进扩展。

别试图放宽 CSP 来适配 dev server——unsafe-eval 在扩展里不被允许,上架也会被拒。

一个小陷阱:popup 的挂载时机

// popup 太小、渲染太快时,可能出现闪烁
const root = createRoot(document.getElementById('app'));

// 先取数据再渲染,避免「空白 → 有内容」的跳动
const { config } = await chrome.storage.local.get('config');
root.render(<App config={config} />);

Popup 的生命周期只有几秒,先取数据后渲染比先渲染骨架屏体验更好——这和普通 Web 应用的直觉相反。

扩展页面的 URL

所有扩展页面都能直接访问:

chrome-extension://<扩展ID>/popup.html

在新标签页打开它调试很方便——但要注意行为不完全一样:以标签页方式打开的 popup 不会被关闭销毁,某些依赖「关闭即销毁」的 bug 在这里复现不出来。

小结

  • Popup 关闭即销毁,状态立刻进 storage,长任务交给 SW。
  • 宽度固定(上限 800×600),没有 alert / confirm。
  • 选项页优先用 options_ui;用户配置存 chrome.storage.sync,但别放密钥。
  • 三种 UI 的判据:要不要边看网页边操作。
  • React/Vue 正常可用,但 dev server 的 HMR 用不了——build --watch 或换 WXT/Plasmo。

下一篇:消息传递。

本页目录