Skip to content

Chrome Extension:Manifest、运行结构、通信、权限与发布 ​

Chrome Extension 直接看成:运行在浏览器里的一个小型应用。

它不是普通网页,也不是传统桌面程序,而是一套建立在浏览器扩展 API 之上的前端应用形态。

它真正解决的是这些问题:

  1. 如何给浏览器增加新能力
  2. 如何在页面里注入交互和自动化逻辑
  3. 如何访问标签页、存储、菜单、通知这些浏览器级能力
  4. 如何把一套前端界面、后台逻辑和页面脚本组合成一个可安装、可发布的浏览器扩展

现在主流扩展开发,基本都要围绕 Manifest V3 来理解。

这篇文章会尽量把 Chrome Extension 这条线讲完整:

  1. 它是什么
  2. 运行结构怎么分
  3. manifest.json 到底在控制什么
  4. 各种脚本上下文怎么通信
  5. 权限、存储、网络能力分别有什么边界
  6. 工程里怎么组织项目、调试、打包和发布

1. Chrome Extension 到底是什么 ​

如果只把它理解成“浏览器插件”,还是有点粗。

实际写项目时,更多是这样:

Chrome Extension 是一套运行在浏览器内部、可以拿到浏览器扩展能力、并按扩展生命周期管理的前端应用。

它和普通网页最大的差别,不在于“是不是也能写 HTML/CSS/JavaScript”,而在于:

  1. 它可以接入浏览器扩展 API
  2. 它可以在特定页面注入脚本
  3. 它可以拥有自己的后台逻辑
  4. 它的权限、生命周期和发布方式都由浏览器扩展系统管理

所以这条线真正要先分清的,不是语法,而是运行位置。


2. Chrome Extension 适合解决什么问题 ​

比较常见的场景包括:

  1. 页面增强
  2. 自动填表
  3. 划词翻译
  4. 广告拦截
  5. 页面数据抓取
  6. 标签页管理
  7. 浏览器侧的工作流自动化
  8. 与后端服务协作的助手类工具

例如:

  1. 在某个网站页面上插入按钮或面板
  2. 读取当前标签页 URL 和页面内容
  3. 根据用户操作发请求、存本地数据、发通知
  4. 在浏览器右键菜单、侧边栏、工具栏里提供入口

要注意的是:

Chrome Extension 很适合做浏览器内的增强和编排,但它不是无限权限的本地程序。


3. 先建立一张运行结构图 ​

Chrome Extension 最容易让人乱的地方,不是 API 多,而是上下文多。

最常见的几类运行上下文包括:

  1. Service Worker
  2. Content Script
  3. Popup
  4. Options Page
  5. 扩展自己的页面,例如 side panel、tab page

看这张总图:

mermaid
flowchart TD
    A[用户点击扩展图标] --> B[Popup]
    B --> C[发送消息给 Service Worker]
    C --> D[调用 chrome.tabs storage alarms 等扩展 API]
    D --> E[必要时给 Content Script 发消息]
    E --> F[在页面 DOM 中执行交互逻辑]

    G[页面加载] --> H[Content Script 注入]
    H --> I[读取页面内容或响应用户动作]
    I --> C

    J[Options Page] --> C
    C --> K[chrome.storage]

这张图最重要的作用,是把几个经常被混成一团的角色拆开:

  1. Popup 更偏用户入口界面
  2. Service Worker 更偏后台协调和浏览器 API 调用
  3. Content Script 更偏贴着页面做 DOM 交互
  4. storage 更偏状态保存

4. Manifest V3 是什么 ​

Manifest 可以理解成扩展的总配置文件。

文件名固定就是:

manifest.json

它的作用不是“写点元信息”,而是:

  1. 告诉浏览器这是一个什么扩展
  2. 告诉浏览器它有哪些入口
  3. 告诉浏览器它要申请哪些权限
  4. 告诉浏览器哪些脚本要注入页面
  5. 告诉浏览器后台逻辑和资源文件分别在哪里

🌟 所以 manifest.json 本质上就是扩展的运行说明书。


5. manifest.json 里最重要的字段 ​

下面这几个字段,基本是最常见也最需要先搞懂的。

字段作用
manifest_version指定扩展规范版本,当前主流是 3
name扩展名称
version扩展版本
description扩展描述
icons扩展图标
action工具栏图标和弹出页入口
background后台脚本配置,MV3 里通常是 service_worker
permissions扩展 API 权限
host_permissions站点访问权限
content_scripts页面注入脚本
options_page 或 options_ui配置页入口
web_accessible_resources允许页面访问的扩展资源

一个最小可运行的示例大概像这样:

json
{
  "manifest_version": 3,
  "name": "Page Inspector Demo",
  "version": "1.0.0",
  "description": "演示扩展的消息通信、存储和页面注入。",
  "action": {
    "default_popup": "popup.html",
    "default_title": "Page Inspector"
  },
  "background": {
    "service_worker": "service-worker.js",
    "type": "module"
  },
  "permissions": [
    "storage",
    "tabs",
    "scripting",
    "activeTab"
  ],
  "host_permissions": [
    "https://*/*",
    "http://*/*"
  ],
  "content_scripts": [
    {
      "matches": ["https://*/*", "http://*/*"],
      "js": ["content-script.js"]
    }
  ],
  "options_page": "options.html"
}

这份配置最值得看懂的是:

  1. 扩展有一个 popup
  2. 有一个后台 service worker
  3. 有一个会注入到页面里的 content script
  4. 申请了标签页、脚本注入和存储权限

6. MV3 为什么强调 Service Worker ​

Manifest V3 最大的变化之一,就是后台能力不再鼓励长期常驻页,而是转向 service worker。

你可以把它理解成:

扩展后台逻辑的事件驱动协调中心。

它通常负责:

  1. 监听扩展事件
  2. 处理消息
  3. 调用浏览器扩展 API
  4. 与存储、标签页、脚本注入能力协作

它和传统网页脚本最大的差别是:

  1. 它没有页面 DOM
  2. 它不是一直常驻的“后台窗口”
  3. 它更偏事件触发后被唤起

所以工程上最容易踩的坑之一就是:

不要把 service worker 当成一个永远在线的内存容器。

例如:

  1. 不要过度依赖内存变量长期存在
  2. 需要保留的状态更适合写入 chrome.storage 或其他持久化方案
  3. 需要定期任务时,更适合配合 alarms 等 API

7. Content Script 到底在做什么 ​

Content Script 可以理解成:

注入到目标网页中的扩展脚本,用来和页面 DOM 打交道。

它很适合做这些事:

  1. 读取页面内容
  2. 修改页面 DOM
  3. 监听用户在页面里的行为
  4. 在页面里挂载扩展 UI

但它也有明显边界:

  1. 它运行在页面上下文附近,但不是页面原生脚本本身
  2. 它能访问 DOM,但并不是所有页面变量都能直接共享
  3. 它能调一部分 chrome.* API,但很多扩展能力仍然更适合通过后台脚本中转

这也是为什么:

  1. 页面交互逻辑放 content script
  2. 浏览器能力编排放 service worker

这个分工通常更稳。


8. Popup、Options、Side Panel 分别适合放什么 ​

8.1 Popup ​

Popup 更像扩展的轻量主入口。

典型场景包括:

  1. 点击工具栏图标后展示当前状态
  2. 触发一次操作
  3. 读取当前标签页信息
  4. 给当前页面发指令

要注意的是:

Popup 生命周期通常很短,用户关掉它就会销毁。

所以:

  1. 不适合把长期状态只放在 popup 内存里
  2. 适合把它做成“展示 + 触发动作”的轻入口

8.2 Options Page ​

它更适合放:

  1. 用户配置
  2. 登录状态设置
  3. 黑白名单
  4. 扩展行为偏好

8.3 Side Panel ​

如果扩展需要更持续的界面交互,例如:

  1. 页面分析助手
  2. AI 辅助面板
  3. 侧边工作流工具

Side Panel 往往会比 popup 更自然。


9. 各种上下文到底怎么分工 ​

前面已经分散提到了几个上下文,但如果没有一张汇总表,后面还是很容易混。

下面这张表更适合快速建立边界感:

上下文核心职责能力边界
Service Worker做后台协调和浏览器能力调度消息中转、标签页操作、脚本注入、右键菜单、定时任务、通知、存储不能直接操作页面 DOM;也不适合把关键状态只放内存
Content Script贴着页面做 DOM 交互读取页面内容、修改 DOM、监听页面行为、挂载页面内 UI不是页面原生脚本;也不适合承担整个扩展后台逻辑
Popup做轻量入口和即时操作展示当前状态、读取当前 tab 信息、触发一次动作、展示最近结果生命周期很短,不适合承担长期状态和持续任务
Options Page做配置管理功能开关、黑白名单、登录配置、偏好项维护不适合承担高频页面注入,也不是后台协调中心
Side Panel做持续交互面板AI 助手、页面分析、长时工具面板、持续展示任务结果不替代 service worker;浏览器级事件编排仍应放后台
content_scripts 配置声明哪些页面要自动注入脚本对固定站点长期增强、自动挂监听逻辑它是声明式自动注入,不等于 chrome.scripting.executeScript 这种动态注入

如果把这张表压缩成几句最好记的结论:

  1. Service Worker 决定扩展“怎么协调、怎么调用浏览器能力”
  2. Content Script 决定扩展“怎么碰页面 DOM”
  3. Popup 决定扩展“用户点图标后看到什么、先做什么”
  4. Options Page 决定扩展“用户怎么改配置”
  5. Side Panel 决定扩展“需不需要一个更持续的工作面板”

9.1 一些最容易混的边界 ​

为了避免读到后面又绕回来,这里再把几组最容易混的边界钉死:

容易混的点更稳的理解
Service Worker 和 Content Script一个偏后台协调和浏览器 API,一个偏页面 DOM
Popup 和 Side Panel一个偏短交互入口,一个偏持续工作面板
Options Page 和 Popup一个偏配置管理,一个偏即时操作入口
content_scripts 和 chrome.scripting.executeScript一个偏声明式自动注入,一个偏按条件动态注入
扩展页面和网页页面都能写 HTML/CSS/JS,但扩展页面是扩展自己的 UI,上下文和权限边界不同

🌟 真正要先立住的分工是:页面逻辑看 content script,扩展后台能力看 service worker,用户入口界面看 popup/options/side panel。


10. 权限模型为什么这么重要 ​

扩展开发里,权限不是“顺手一配”,而是整个能力边界的一部分。

常见权限大致分三类:

  1. 扩展 API 权限
  2. 站点访问权限
  3. 可选权限

10.1 permissions ​

例如:

  1. storage
  2. tabs
  3. scripting
  4. contextMenus
  5. notifications
  6. alarms

它们主要控制扩展能不能调用某些浏览器扩展 API。

10.2 host_permissions ​

它控制的是:

扩展能访问哪些站点。

例如:

json
"host_permissions": [
  "https://*.example.com/*"
]

它比直接写全网通配更稳,因为:

  1. 权限范围更清楚
  2. 发布审核更容易过
  3. 用户也更容易理解你到底要访问哪些站点

10.3 activeTab ​

这是一个很常见、也很实用的权限。

可以看成:

用户主动和扩展交互后,临时获得当前标签页的访问能力。

它适合:

  1. 点击按钮后对当前页面执行一次分析
  2. 注入一次脚本
  3. 读取一次当前页信息

相比全局长期站点权限,它更轻一些。


11. 消息通信为什么一定要讲清楚 ​

扩展的几个运行上下文默认不是一个大一统脚本。

所以只要上下文一多,就一定会遇到:

  1. popup 怎么通知后台
  2. 后台怎么通知 content script
  3. content script 怎么把页面结果回传给 popup

这就是消息通信的意义。

最常见的几种方式包括:

  1. 一次性消息 runtime.sendMessage
  2. 长连接 runtime.connect
  3. 借助 chrome.storage 做状态同步
  4. 需要时再配合页面脚本注入和 window.postMessage

12. 一次典型消息链路长什么样 ​

看一条最常见的主线:

mermaid
sequenceDiagram
    participant U as 用户
    participant P as Popup
    participant SW as Service Worker
    participant CS as Content Script
    participant S as chrome.storage

    U->>P: 点击“抓取当前页面标题”
    P->>SW: sendMessage(action=get-page-title)
    SW->>CS: tabs.sendMessage
    CS->>CS: 读取 document.title
    CS-->>SW: 返回标题
    SW->>S: 存储最近一次结果
    SW-->>P: 返回标题

这条链路最重要的作用,是帮你建立一个稳定认识:

  1. popup 不一定直接碰页面 DOM
  2. content script 更适合贴着页面拿数据
  3. service worker 更适合做协调和存储

13. 一段最小通信示例 ​

下面这组代码示例,展示的是:

  1. popup 发消息
  2. service worker 转发
  3. content script 读页面标题
  4. 结果回写给 popup

13.1 popup.js ​

js
/**
 * Popup 入口脚本。
 * 负责给当前激活标签页发起“读取标题”的动作。
 */
const resultNode = document.querySelector('#result');
const fetchButton = document.querySelector('#fetchTitle');

fetchButton.addEventListener('click', async () => {
  const response = await chrome.runtime.sendMessage({
    action: 'get-page-title'
  });

  resultNode.textContent = response?.title || '读取失败';
});

13.2 service-worker.js ​

js
/**
 * 扩展后台协调脚本。
 * 负责接收 popup 请求、找到当前激活标签页,并把消息转给 content script。
 */
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.action !== 'get-page-title') {
    return;
  }

  chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
    const currentTab = tabs[0];

    if (!currentTab?.id) {
      sendResponse({ title: '' });
      return;
    }

    chrome.tabs.sendMessage(
      currentTab.id,
      { action: 'read-page-title' },
      async (pageResponse) => {
        const title = pageResponse?.title || '';

        // 把最近一次页面标题写入扩展存储,避免只停留在内存里
        await chrome.storage.local.set({ lastPageTitle: title });
        sendResponse({ title });
      }
    );
  });

  // 这里返回 true,表示会异步调用 sendResponse
  return true;
});

13.3 content-script.js ​

js
/**
 * 页面注入脚本。
 * 负责读取当前页面 DOM,并把结果回传给扩展后台。
 */
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.action !== 'read-page-title') {
    return;
  }

  sendResponse({
    title: document.title
  });
});

这段示例真正想说明的,不只是 API 怎么调,而是:

  1. popup 是用户入口
  2. service worker 是协调中心
  3. content script 才是页面 DOM 的直接读取者

14. 存储应该怎么选 ​

扩展里常见的存储方案包括:

  1. chrome.storage.local
  2. chrome.storage.sync
  3. chrome.storage.session
  4. IndexedDB
  5. 少量场景下使用页面自己的存储能力

14.1 storage.local ​

最常用。

它适合:

  1. 配置项
  2. 中等规模本地数据
  3. 最近一次操作结果
  4. 用户在本机上的持久状态

14.2 storage.sync ​

它更适合:

  1. 小体量配置
  2. 需要跟随 Chrome 账号同步的偏好项

但不要把它当大数据仓库。

14.3 storage.session ​

它更像扩展会话级状态。

适合:

  1. 临时状态
  2. 不需要长期落盘的数据

14.4 IndexedDB ​

如果你的扩展要处理:

  1. 更复杂的数据结构
  2. 更大体量的本地数据
  3. 检索和离线缓存

IndexedDB 往往更合适。

🌟 先记结论就够了:小到中等配置和状态优看 chrome.storage,数据量更大或结构更复杂时再看 IndexedDB。


15. 网络请求和页面请求边界怎么分 ​

扩展开发里,网络请求很容易和网页自身请求混在一起。

真正要先分清的是:

  1. 是页面自己在请求
  2. 还是扩展在请求

15.1 后台脚本发请求 ​

如果是扩展后台逻辑发请求,它通常更适合:

  1. 请求业务后端接口
  2. 做统一鉴权
  3. 避免把敏感调用逻辑全塞在 content script

15.2 content script 发请求 ​

能发,但要更留意:

  1. 当前页面环境
  2. 站点权限
  3. 请求边界和安全问题

15.3 页面 DOM 和页面 JS 变量不是一回事 ​

很多人会误以为 content script 既然进入了页面,就能像页面原始脚本一样直接共享一切。

其实不是。

更稳的理解是:

  1. content script 很适合操作页面 DOM
  2. 但页面脚本本身的运行上下文和扩展脚本并不是完全同一个世界

如果真要和页面原始脚本通信,通常要再借:

  1. 注入页面脚本
  2. window.postMessage
  3. 自定义 DOM 事件

16. 常见扩展 API 应该怎么分组理解 ​

如果一上来就背一大串 chrome.* API,很容易乱。

更稳的办法不是按“API 名字”记,而是按“我要解决什么问题”来记。

看这张表:

我现在要做什么更常用的 API更适合放在哪个上下文
找到当前标签页、切标签、拿 URLchrome.tabsservice worker、popup
动态注入脚本或样式chrome.scriptingservice worker
监听页面安装、消息、生命周期chrome.runtimeservice worker
存配置、同步状态chrome.storage几乎所有扩展上下文
管理右键菜单、图标点击、侧边栏chrome.contextMenus、chrome.action、chrome.sidePanelservice worker
做定时任务和快捷键chrome.alarms、chrome.commandsservice worker
发通知chrome.notificationsservice worker
拿 Cookie、会话、下载chrome.cookies、chrome.sessions、chrome.downloadsservice worker
管理网络规则declarativeNetRequestservice worker + manifest

🌟 真正要先建立的是这层判断:页面 DOM 相关优先想 content script,浏览器级能力优先想 service worker,用户触发入口再看 popup、action、contextMenus、side panel。

16.1 先分清“谁能调什么” ​

扩展 API 虽然都叫 chrome.*,但不是每个上下文都适合同样的调用方式。

更常见的分工大致是:

  1. Service Worker
    • 更适合调 tabs、scripting、contextMenus、alarms、notifications、cookies
    • 更适合监听 runtime 生命周期事件
  2. Popup / Options / Side Panel
    • 更适合做 UI 和用户触发
    • 可以调用部分扩展 API,但通常不负责全局协调
  3. Content Script
    • 更适合读写 DOM、监听页面交互
    • 适合少量调用扩展能力,但不适合把它做成后台总线

这也是为什么很多扩展一复杂,最后都会收敛成这条主线:

  1. 用户在 popup 或页面里点击
  2. 请求发给 service worker
  3. service worker 再去调用浏览器 API
  4. 需要碰 DOM 时再转给 content script

16.2 runtime 是扩展的总入口 ​

chrome.runtime 基本可以看成扩展 API 的总线入口。

它最常负责:

  1. 生命周期事件
  2. 消息通信
  3. 扩展基础信息
  4. 打开 options 页、拿运行时 URL 等通用能力

最常见的几个事件包括:

  1. chrome.runtime.onInstalled
  2. chrome.runtime.onMessage
  3. chrome.runtime.onConnect
  4. chrome.runtime.onStartup

例如,安装完成时初始化右键菜单:

js
/**
 * 扩展安装后初始化菜单。
 * 这里只做一次性初始化,避免每次 service worker 唤起都重复创建。
 */
chrome.runtime.onInstalled.addListener(() => {
  chrome.contextMenus.create({
    id: 'inspect-selection',
    title: '分析当前选中文本',
    contexts: ['selection']
  });
});

这组 API 真正的价值不只是“能监听事件”,而是:

它把扩展生命周期、通信入口和全局初始化串到了一起。

16.3 tabs、windows、scripting 是页面控制主线 ​

如果你的扩展要和浏览器页面打交道,这三组 API 基本最常见。

16.3.1 chrome.tabs ​

它主要解决:

  1. 当前标签页是谁
  2. 标签页 URL、标题、激活状态是什么
  3. 我要不要切换、更新、发送消息给这个 tab

最常见的场景:

  1. popup 里读取当前页信息
  2. 在后台脚本里给指定 tab 发消息
  3. 根据当前页 URL 决定扩展是否可用

16.3.2 chrome.windows ​

它更偏窗口级管理:

  1. 当前窗口是谁
  2. 开新窗口
  3. 控制窗口状态和尺寸

普通扩展不一定高频用到,但做标签页管理器、多窗口工具时会明显增加使用频率。

16.3.3 chrome.scripting ​

这是 MV3 里非常关键的一组能力。

它主要解决:

  1. 动态注入脚本
  2. 动态注入样式
  3. 在目标 tab 中执行一段函数逻辑

它和 content_scripts 的区别可以这样理解:

  1. content_scripts 更像“提前声明好,命中页面就自动注入”
  2. chrome.scripting 更像“用户触发后,我再按条件动态注入”

这两种方式都常见,但使用时机不一样。

下面这段示例展示的是:用户点击扩展图标后,只对当前活动标签页注入一段高亮脚本。

js
/**
 * 点击扩展图标后,给当前页面里所有 h1 加一个高亮边框。
 * 这里适合用 activeTab + scripting,而不是给所有页面长期注入脚本。
 */
chrome.action.onClicked.addListener(async (tab) => {
  if (!tab.id) {
    return;
  }

  await chrome.scripting.executeScript({
    target: { tabId: tab.id },
    func: () => {
      document.querySelectorAll('h1').forEach((node) => {
        node.style.outline = '2px solid #1677ff';
      });
    }
  });
});

这类写法特别适合:

  1. 用户主动触发一次动作
  2. 不想让脚本长期驻留在所有页面
  3. 配合 activeTab 做更轻的权限申请

16.4 storage、cookies、sessions 是状态层 ​

这一层更像扩展自己的状态基础设施。

16.4.1 chrome.storage ​

前面已经讲过它的选型,这里再补一个更贴近工程落地的理解:

  1. storage.local 更像本机扩展配置中心
  2. storage.sync 更像小体量偏好同步层
  3. storage.session 更像扩展会话内的短状态区

它常见的工程用法包括:

  1. 保存用户设置
  2. 保存最近一次抓取结果
  3. 在 popup 和 service worker 之间同步状态
  4. 给 content script 提供配置读取入口

16.4.2 chrome.cookies ​

它更适合:

  1. 需要读取站点 Cookie 的扩展
  2. 需要配合后端会话做辅助判断的场景

要注意的是:

  1. 它权限更敏感
  2. 站点边界要配清楚
  3. 不要把它当成“想读什么 Cookie 都能随便读”

16.4.3 chrome.sessions ​

它更偏浏览器会话信息,例如:

  1. 最近关闭的标签页
  2. 最近关闭的窗口

如果做“恢复工作流”“找回刚关闭页面”这类扩展,它会比较有用。

16.5 action、contextMenus、sidePanel、notifications 是用户入口层 ​

这一组 API 可以看成:扩展和用户碰面的地方。

16.5.1 chrome.action ​

它主要对应浏览器工具栏上的扩展图标。

常见用途:

  1. 点击图标打开 popup
  2. 动态修改图标状态
  3. 修改 badge 文本和颜色

例如:

  1. 登录后显示 ON
  2. 当前站点不支持时置灰
  3. 有新结果时显示计数角标

16.5.2 chrome.contextMenus ​

它特别适合做:

  1. 右键菜单增强
  2. 对选中文本、图片、链接执行动作
  3. 页面工作流入口

例如“选中文本后翻译”“右键图片后收藏”“右键链接后发送到待办”这类场景,都很适合从这里起步。

16.5.3 chrome.sidePanel ​

它适合:

  1. 需要持续可见的工具面板
  2. 页面助手、AI 助手、分析面板
  3. 比 popup 更长生命周期的交互界面

如果你的扩展交互比较复杂,side panel 往往比 popup 更自然。

16.5.4 chrome.notifications ​

它主要用来:

  1. 提示任务完成
  2. 提醒错误或异常
  3. 给后台任务提供轻提醒

但不要滥用,不然很容易把扩展做成打扰源。

16.6 alarms、commands、downloads 更偏调度和工具能力 ​

16.6.1 chrome.alarms ​

它适合:

  1. 定时轮询
  2. 周期清理缓存
  3. 到点提醒
  4. 延迟触发后台任务

它的价值在于:service worker 不是常驻的,但定时事件仍然可以靠 alarms 重新唤起处理逻辑。

16.6.2 chrome.commands ​

它主要解决:

  1. 给扩展绑定快捷键
  2. 让高频动作更快触发

例如:

  1. 快捷打开侧边栏
  2. 快捷抓取当前页面标题
  3. 快捷执行页面高亮

16.6.3 chrome.downloads ​

如果扩展有导出文件、下载报告、保存抓取结果这类需求,这组 API 很常用。

它比“直接让页面下一个文件”更适合由扩展后台统一接管下载行为。

16.7 declarativeNetRequest 是 MV3 下的网络规则主线 ​

以前很多人会把“扩展拦请求”理解成脚本里手动拦截。

但在 MV3 语境下,更需要优先理解的是:

declarativeNetRequest 更像浏览器内部的声明式网络规则系统。

它更适合:

  1. 请求拦截
  2. 请求头修改
  3. URL 重定向
  4. 广告和追踪规则过滤

它和普通脚本请求的差别在于:

  1. 不是每次请求都由你手写 JS 即时处理
  2. 而是把规则声明给浏览器,再由浏览器按规则执行

这样做的好处是:

  1. 更符合 MV3 约束
  2. 性能更稳
  3. 行为边界更清楚

但这一层也要更小心:

  1. 规则能力更敏感
  2. 审核更严格
  3. 权限说明必须更清楚

16.8 一组更实用的 API 组合思路 ​

真正做扩展时,通常不是单独用一个 API,而是几组 API 配合。

比较常见的组合有:

  1. action + tabs + scripting
    • 用户点击图标
    • 找到当前 tab
    • 动态注入脚本
  2. contextMenus + tabs + runtime
    • 用户右键触发动作
    • 后台收到菜单点击事件
    • 给当前页面或 popup 发消息
  3. runtime + storage + sidePanel
    • 页面或 popup 把结果发给后台
    • 后台存入状态
    • side panel 读取并持续展示
  4. alarms + notifications + storage
    • 定时任务触发
    • 后台更新状态
    • 再用通知提示用户
  5. tabs + cookies + fetch
    • 根据当前站点做辅助判断
    • 结合登录态或站点上下文访问后端能力

如果把这些组合压缩成一句话,就是:

Chrome Extension API 真正难的不是单个方法怎么调,而是要把入口、后台、页面、状态和权限串成一条稳定主线。

16.9 一个更接近工程场景的小例子 ​

下面这段代码演示的是:

  1. 安装扩展时注册右键菜单
  2. 用户选中文本后点击菜单
  3. 后台读取当前 tab 的选中文本
  4. 把结果写进 storage
  5. 后续 popup 或 side panel 可以直接读取
js
/**
 * 安装时创建右键菜单。
 */
chrome.runtime.onInstalled.addListener(() => {
  chrome.contextMenus.create({
    id: 'save-selection',
    title: '保存选中文本到扩展',
    contexts: ['selection']
  });
});

/**
 * 用户点击右键菜单后,把当前选中文本持久化到 storage.local。
 */
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
  if (info.menuItemId !== 'save-selection') {
    return;
  }

  const selectionText = info.selectionText || '';

  await chrome.storage.local.set({
    lastSelection: {
      text: selectionText,
      pageTitle: tab?.title || '',
      url: tab?.url || '',
      updatedAt: Date.now()
    }
  });

  await chrome.notifications.create({
    type: 'basic',
    iconUrl: 'icons/128.png',
    title: '保存成功',
    message: '当前选中文本已经写入扩展存储。'
  });
});

这段代码真正想说明的是:

  1. 用户入口可以来自右键菜单,而不一定来自 popup
  2. 后台脚本可以直接拿到菜单点击信息和 tab 上下文
  3. storage、contextMenus、notifications 这几组 API 在工程里经常是连着用的

16.10 学 API 时最容易走偏的地方 ​

最常见的误区有这几个:

  1. 看到什么都想放进 content script
  2. 只会调方法,不知道该在哪个上下文里调
  3. 只会申请权限,不知道为什么需要这些权限
  4. 把 content_scripts 和 scripting.executeScript 当成完全相同的东西
  5. 只会让 popup 直接干活,不做后台协调

如果把这几个误区避开,Chrome Extension API 这条线会顺很多。


17. 项目结构怎么组织更稳 ​

如果扩展稍微复杂一点,更适合按运行上下文拆目录,而不是把所有文件扔在一起。

例如:

text
chrome-extension-demo/
├── manifest.json
├── public/
│   └── icons/
├── src/
│   ├── background/
│   │   └── service-worker.ts
│   ├── content/
│   │   └── content-script.ts
│   ├── popup/
│   │   ├── popup.html
│   │   └── popup.ts
│   ├── options/
│   │   ├── options.html
│   │   └── options.ts
│   ├── shared/
│   │   ├── message-types.ts
│   │   └── storage-keys.ts
│   └── utils/
│       └── browser.ts
└── package.json

这样拆的好处是:

  1. 一眼能看出不同上下文
  2. 共享类型和常量有独立位置
  3. 后续接 TypeScript、React、Vite 时也更容易接

18. 工程里为什么常配合 Vite、React、TypeScript ​

Chrome Extension 本身不强制你用什么框架。

但现实项目里,下面这套组合很常见:

  1. TypeScript
  2. Vite
  3. React 或其他 UI 框架

原因不复杂:

  1. popup、options、side panel 本身就是小型前端页面
  2. TypeScript 适合管理消息类型、权限配置、存储键名
  3. Vite 适合组织多入口构建

要注意的是:

扩展是浏览器平台应用,不是普通 SPA。

所以即使用 React/Vite,也不能忘了:

  1. 仍然有 manifest.json
  2. 仍然有 content script
  3. 仍然有 service worker
  4. 仍然有权限模型

19. 调试时最常见的几条线 ​

19.1 调 popup ​

看 popup 自己的页面控制台。

19.2 调 service worker ​

去扩展管理页里看后台 service worker 的调试入口。

19.3 调 content script ​

直接在目标网页 DevTools 里看注入脚本行为。

19.4 调权限和 manifest ​

很多问题根本不是代码逻辑错,而是:

  1. 权限没配
  2. 匹配规则没命中
  3. 资源没声明
  4. 修改后没重新加载扩展

🌟 所以扩展调试一定要把“代码问题”和“配置问题”分开排。


20. 发布到 Chrome Web Store 前要检查什么 ​

发布不是把 zip 一传就结束。

至少要检查这些:

  1. 图标、名称、描述是否完整
  2. 权限是否最小化
  3. 是否误申请了过大的站点权限
  4. 是否包含无关调试代码
  5. 是否把本地密钥、测试地址、账号信息打包进去了
  6. 是否有清楚的隐私说明
  7. 如果涉及远程请求、账号体系、用户数据,要把用途写清楚

真正影响审核通过率的,往往不是 UI,而是:

  1. 权限理由是否充分
  2. 数据使用是否透明
  3. 行为是否和描述一致

21. 安全边界为什么比普通前端更重要 ​

因为扩展天然就更靠近浏览器能力。

真正要特别注意的是:

  1. 不要随便注入不可信脚本
  2. 不要把敏感 token 直接暴露给页面上下文
  3. 不要申请远超实际需求的权限
  4. 不要把远程执行逻辑做成不透明黑盒
  5. 对用户数据采集和上传必须说清楚

可以把这条原则记住:

普通网页更多是在自己的站点边界里做事,扩展是在浏览器边界里做事,所以权限和信任问题会更敏感。


22. 最常见的几个坑 ​

22.1 把 service worker 当常驻后台 ​

这会导致:

  1. 内存状态丢失时一脸懵
  2. 调试时觉得“刚才还能用,怎么又没了”

22.2 把页面脚本、content script、扩展后台脚本混成同一个世界 ​

这会导致:

  1. DOM 能拿到,但页面变量拿不到
  2. 消息链路绕不清

22.3 权限申请过大 ​

这会带来:

  1. 用户不信任
  2. 审核更难过
  3. 安全边界更难控制

22.4 只会写 UI,不理解 manifest 和上下文分工 ​

这样扩展一复杂,马上就会乱。

22.5 只看 API,不看生命周期 ​

扩展很多 bug 不是“不会调 API”,而是:

  1. 在错误的上下文里调
  2. 在错误的生命周期节点里调

23. 一份更实用的 Chrome Extension 检查清单 ​

在真正开始做一个扩展前,至少把下面这些问题想清楚:

  1. 这个能力更适合放在 popup、content script 还是 service worker
  2. 页面 DOM 操作和后台能力调用的边界是不是清楚
  3. 需要哪些权限,能不能再缩小
  4. 需要持久化的数据放哪里
  5. 消息链路是不是只有一条主线,能不能画出来
  6. 当前场景到底是页面增强、浏览器能力调用,还是后端协同工具
  7. UI 是短交互入口还是持续面板
  8. 调试时该去哪个上下文看日志
  9. 发布时是否会因为权限和隐私说明被卡住

24. 总结 ​

如果把 Chrome Extension 这条线压缩成最核心的几句话,就是:

  1. 它是一种运行在浏览器里的前端应用形态
  2. manifest.json 决定扩展的入口、权限和运行配置
  3. service worker、content script、popup 是最核心的三个上下文
  4. 页面 DOM、浏览器 API、用户界面这三条线必须分工清楚
  5. 权限、通信、存储、生命周期和发布审核,是扩展开发里最容易把项目拉开的地方

真正学 Chrome Extension,不是背一堆 chrome.* API,而是把:

运行结构、上下文边界、权限模型和工程落地方式

这四件事理顺。

基于 VitePress 构建的个人技术笔记。