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 paneltab 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_pageoptions_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. PopupOptionsSide 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 WorkerContent Script一个偏后台协调和浏览器 API,一个偏页面 DOM
PopupSide Panel一个偏短交互入口,一个偏持续工作面板
Options PagePopup一个偏配置管理,一个偏即时操作入口
content_scriptschrome.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.contextMenuschrome.actionchrome.sidePanelservice worker
做定时任务和快捷键chrome.alarmschrome.commandsservice worker
发通知chrome.notificationsservice worker
拿 Cookie、会话、下载chrome.cookieschrome.sessionschrome.downloadsservice worker
管理网络规则declarativeNetRequestservice worker + manifest

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

16.1 先分清“谁能调什么”

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

更常见的分工大致是:

  1. Service Worker
    • 更适合调 tabsscriptingcontextMenusalarmsnotificationscookies
    • 更适合监听 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 tabswindowsscripting 是页面控制主线

如果你的扩展要和浏览器页面打交道,这三组 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 storagecookiessessions 是状态层

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

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 actioncontextMenussidePanelnotifications 是用户入口层

这一组 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 alarmscommandsdownloads 更偏调度和工具能力

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. storagecontextMenusnotifications 这几组 API 在工程里经常是连着用的

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

最常见的误区有这几个:

  1. 看到什么都想放进 content script
  2. 只会调方法,不知道该在哪个上下文里调
  3. 只会申请权限,不知道为什么需要这些权限
  4. content_scriptsscripting.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 workercontent scriptpopup 是最核心的三个上下文
  4. 页面 DOM、浏览器 API、用户界面这三条线必须分工清楚
  5. 权限、通信、存储、生命周期和发布审核,是扩展开发里最容易把项目拉开的地方

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

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

这四件事理顺。

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