Appearance
Chrome Extension:Manifest、运行结构、通信、权限与发布
Chrome Extension 直接看成:运行在浏览器里的一个小型应用。
它不是普通网页,也不是传统桌面程序,而是一套建立在浏览器扩展 API 之上的前端应用形态。
它真正解决的是这些问题:
- 如何给浏览器增加新能力
- 如何在页面里注入交互和自动化逻辑
- 如何访问标签页、存储、菜单、通知这些浏览器级能力
- 如何把一套前端界面、后台逻辑和页面脚本组合成一个可安装、可发布的浏览器扩展
现在主流扩展开发,基本都要围绕 Manifest V3 来理解。
这篇文章会尽量把 Chrome Extension 这条线讲完整:
- 它是什么
- 运行结构怎么分
manifest.json到底在控制什么- 各种脚本上下文怎么通信
- 权限、存储、网络能力分别有什么边界
- 工程里怎么组织项目、调试、打包和发布
1. Chrome Extension 到底是什么
如果只把它理解成“浏览器插件”,还是有点粗。
实际写项目时,更多是这样:
Chrome Extension 是一套运行在浏览器内部、可以拿到浏览器扩展能力、并按扩展生命周期管理的前端应用。
它和普通网页最大的差别,不在于“是不是也能写 HTML/CSS/JavaScript”,而在于:
- 它可以接入浏览器扩展 API
- 它可以在特定页面注入脚本
- 它可以拥有自己的后台逻辑
- 它的权限、生命周期和发布方式都由浏览器扩展系统管理
所以这条线真正要先分清的,不是语法,而是运行位置。
2. Chrome Extension 适合解决什么问题
比较常见的场景包括:
- 页面增强
- 自动填表
- 划词翻译
- 广告拦截
- 页面数据抓取
- 标签页管理
- 浏览器侧的工作流自动化
- 与后端服务协作的助手类工具
例如:
- 在某个网站页面上插入按钮或面板
- 读取当前标签页 URL 和页面内容
- 根据用户操作发请求、存本地数据、发通知
- 在浏览器右键菜单、侧边栏、工具栏里提供入口
要注意的是:
Chrome Extension 很适合做浏览器内的增强和编排,但它不是无限权限的本地程序。
3. 先建立一张运行结构图
Chrome Extension 最容易让人乱的地方,不是 API 多,而是上下文多。
最常见的几类运行上下文包括:
Service WorkerContent ScriptPopupOptions Page- 扩展自己的页面,例如
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]这张图最重要的作用,是把几个经常被混成一团的角色拆开:
Popup更偏用户入口界面Service Worker更偏后台协调和浏览器 API 调用Content Script更偏贴着页面做 DOM 交互storage更偏状态保存
4. Manifest V3 是什么
Manifest 可以理解成扩展的总配置文件。
文件名固定就是:
manifest.json
它的作用不是“写点元信息”,而是:
- 告诉浏览器这是一个什么扩展
- 告诉浏览器它有哪些入口
- 告诉浏览器它要申请哪些权限
- 告诉浏览器哪些脚本要注入页面
- 告诉浏览器后台逻辑和资源文件分别在哪里
🌟 所以 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"
}这份配置最值得看懂的是:
- 扩展有一个
popup - 有一个后台
service worker - 有一个会注入到页面里的
content script - 申请了标签页、脚本注入和存储权限
6. MV3 为什么强调 Service Worker
Manifest V3 最大的变化之一,就是后台能力不再鼓励长期常驻页,而是转向 service worker。
你可以把它理解成:
扩展后台逻辑的事件驱动协调中心。
它通常负责:
- 监听扩展事件
- 处理消息
- 调用浏览器扩展 API
- 与存储、标签页、脚本注入能力协作
它和传统网页脚本最大的差别是:
- 它没有页面 DOM
- 它不是一直常驻的“后台窗口”
- 它更偏事件触发后被唤起
所以工程上最容易踩的坑之一就是:
不要把 service worker 当成一个永远在线的内存容器。
例如:
- 不要过度依赖内存变量长期存在
- 需要保留的状态更适合写入
chrome.storage或其他持久化方案 - 需要定期任务时,更适合配合
alarms等 API
7. Content Script 到底在做什么
Content Script 可以理解成:
注入到目标网页中的扩展脚本,用来和页面 DOM 打交道。
它很适合做这些事:
- 读取页面内容
- 修改页面 DOM
- 监听用户在页面里的行为
- 在页面里挂载扩展 UI
但它也有明显边界:
- 它运行在页面上下文附近,但不是页面原生脚本本身
- 它能访问 DOM,但并不是所有页面变量都能直接共享
- 它能调一部分
chrome.*API,但很多扩展能力仍然更适合通过后台脚本中转
这也是为什么:
- 页面交互逻辑放
content script - 浏览器能力编排放
service worker
这个分工通常更稳。
8. Popup、Options、Side Panel 分别适合放什么
8.1 Popup
Popup 更像扩展的轻量主入口。
典型场景包括:
- 点击工具栏图标后展示当前状态
- 触发一次操作
- 读取当前标签页信息
- 给当前页面发指令
要注意的是:
Popup 生命周期通常很短,用户关掉它就会销毁。
所以:
- 不适合把长期状态只放在 popup 内存里
- 适合把它做成“展示 + 触发动作”的轻入口
8.2 Options Page
它更适合放:
- 用户配置
- 登录状态设置
- 黑白名单
- 扩展行为偏好
8.3 Side Panel
如果扩展需要更持续的界面交互,例如:
- 页面分析助手
- AI 辅助面板
- 侧边工作流工具
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 这种动态注入 |
如果把这张表压缩成几句最好记的结论:
Service Worker决定扩展“怎么协调、怎么调用浏览器能力”Content Script决定扩展“怎么碰页面 DOM”Popup决定扩展“用户点图标后看到什么、先做什么”Options Page决定扩展“用户怎么改配置”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. 权限模型为什么这么重要
扩展开发里,权限不是“顺手一配”,而是整个能力边界的一部分。
常见权限大致分三类:
- 扩展 API 权限
- 站点访问权限
- 可选权限
10.1 permissions
例如:
storagetabsscriptingcontextMenusnotificationsalarms
它们主要控制扩展能不能调用某些浏览器扩展 API。
10.2 host_permissions
它控制的是:
扩展能访问哪些站点。
例如:
json
"host_permissions": [
"https://*.example.com/*"
]它比直接写全网通配更稳,因为:
- 权限范围更清楚
- 发布审核更容易过
- 用户也更容易理解你到底要访问哪些站点
10.3 activeTab
这是一个很常见、也很实用的权限。
可以看成:
用户主动和扩展交互后,临时获得当前标签页的访问能力。
它适合:
- 点击按钮后对当前页面执行一次分析
- 注入一次脚本
- 读取一次当前页信息
相比全局长期站点权限,它更轻一些。
11. 消息通信为什么一定要讲清楚
扩展的几个运行上下文默认不是一个大一统脚本。
所以只要上下文一多,就一定会遇到:
- popup 怎么通知后台
- 后台怎么通知 content script
- content script 怎么把页面结果回传给 popup
这就是消息通信的意义。
最常见的几种方式包括:
- 一次性消息
runtime.sendMessage - 长连接
runtime.connect - 借助
chrome.storage做状态同步 - 需要时再配合页面脚本注入和
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: 返回标题这条链路最重要的作用,是帮你建立一个稳定认识:
- popup 不一定直接碰页面 DOM
- content script 更适合贴着页面拿数据
- service worker 更适合做协调和存储
13. 一段最小通信示例
下面这组代码示例,展示的是:
- popup 发消息
- service worker 转发
- content script 读页面标题
- 结果回写给 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 怎么调,而是:
- popup 是用户入口
- service worker 是协调中心
- content script 才是页面 DOM 的直接读取者
14. 存储应该怎么选
扩展里常见的存储方案包括:
chrome.storage.localchrome.storage.syncchrome.storage.sessionIndexedDB- 少量场景下使用页面自己的存储能力
14.1 storage.local
最常用。
它适合:
- 配置项
- 中等规模本地数据
- 最近一次操作结果
- 用户在本机上的持久状态
14.2 storage.sync
它更适合:
- 小体量配置
- 需要跟随 Chrome 账号同步的偏好项
但不要把它当大数据仓库。
14.3 storage.session
它更像扩展会话级状态。
适合:
- 临时状态
- 不需要长期落盘的数据
14.4 IndexedDB
如果你的扩展要处理:
- 更复杂的数据结构
- 更大体量的本地数据
- 检索和离线缓存
IndexedDB 往往更合适。
🌟 先记结论就够了:小到中等配置和状态优看 chrome.storage,数据量更大或结构更复杂时再看 IndexedDB。
15. 网络请求和页面请求边界怎么分
扩展开发里,网络请求很容易和网页自身请求混在一起。
真正要先分清的是:
- 是页面自己在请求
- 还是扩展在请求
15.1 后台脚本发请求
如果是扩展后台逻辑发请求,它通常更适合:
- 请求业务后端接口
- 做统一鉴权
- 避免把敏感调用逻辑全塞在 content script
15.2 content script 发请求
能发,但要更留意:
- 当前页面环境
- 站点权限
- 请求边界和安全问题
15.3 页面 DOM 和页面 JS 变量不是一回事
很多人会误以为 content script 既然进入了页面,就能像页面原始脚本一样直接共享一切。
其实不是。
更稳的理解是:
- content script 很适合操作页面 DOM
- 但页面脚本本身的运行上下文和扩展脚本并不是完全同一个世界
如果真要和页面原始脚本通信,通常要再借:
- 注入页面脚本
window.postMessage- 自定义 DOM 事件
16. 常见扩展 API 应该怎么分组理解
如果一上来就背一大串 chrome.* API,很容易乱。
更稳的办法不是按“API 名字”记,而是按“我要解决什么问题”来记。
看这张表:
| 我现在要做什么 | 更常用的 API | 更适合放在哪个上下文 |
|---|---|---|
| 找到当前标签页、切标签、拿 URL | chrome.tabs | service worker、popup |
| 动态注入脚本或样式 | chrome.scripting | service worker |
| 监听页面安装、消息、生命周期 | chrome.runtime | service worker |
| 存配置、同步状态 | chrome.storage | 几乎所有扩展上下文 |
| 管理右键菜单、图标点击、侧边栏 | chrome.contextMenus、chrome.action、chrome.sidePanel | service worker |
| 做定时任务和快捷键 | chrome.alarms、chrome.commands | service worker |
| 发通知 | chrome.notifications | service worker |
| 拿 Cookie、会话、下载 | chrome.cookies、chrome.sessions、chrome.downloads | service worker |
| 管理网络规则 | declarativeNetRequest | service worker + manifest |
🌟 真正要先建立的是这层判断:页面 DOM 相关优先想 content script,浏览器级能力优先想 service worker,用户触发入口再看 popup、action、contextMenus、side panel。
16.1 先分清“谁能调什么”
扩展 API 虽然都叫 chrome.*,但不是每个上下文都适合同样的调用方式。
更常见的分工大致是:
Service Worker- 更适合调
tabs、scripting、contextMenus、alarms、notifications、cookies - 更适合监听
runtime生命周期事件
- 更适合调
Popup / Options / Side Panel- 更适合做 UI 和用户触发
- 可以调用部分扩展 API,但通常不负责全局协调
Content Script- 更适合读写 DOM、监听页面交互
- 适合少量调用扩展能力,但不适合把它做成后台总线
这也是为什么很多扩展一复杂,最后都会收敛成这条主线:
- 用户在
popup或页面里点击 - 请求发给
service worker service worker再去调用浏览器 API- 需要碰 DOM 时再转给
content script
16.2 runtime 是扩展的总入口
chrome.runtime 基本可以看成扩展 API 的总线入口。
它最常负责:
- 生命周期事件
- 消息通信
- 扩展基础信息
- 打开 options 页、拿运行时 URL 等通用能力
最常见的几个事件包括:
chrome.runtime.onInstalledchrome.runtime.onMessagechrome.runtime.onConnectchrome.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
它主要解决:
- 当前标签页是谁
- 标签页 URL、标题、激活状态是什么
- 我要不要切换、更新、发送消息给这个 tab
最常见的场景:
- popup 里读取当前页信息
- 在后台脚本里给指定 tab 发消息
- 根据当前页 URL 决定扩展是否可用
16.3.2 chrome.windows
它更偏窗口级管理:
- 当前窗口是谁
- 开新窗口
- 控制窗口状态和尺寸
普通扩展不一定高频用到,但做标签页管理器、多窗口工具时会明显增加使用频率。
16.3.3 chrome.scripting
这是 MV3 里非常关键的一组能力。
它主要解决:
- 动态注入脚本
- 动态注入样式
- 在目标 tab 中执行一段函数逻辑
它和 content_scripts 的区别可以这样理解:
content_scripts更像“提前声明好,命中页面就自动注入”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';
});
}
});
});这类写法特别适合:
- 用户主动触发一次动作
- 不想让脚本长期驻留在所有页面
- 配合
activeTab做更轻的权限申请
16.4 storage、cookies、sessions 是状态层
这一层更像扩展自己的状态基础设施。
16.4.1 chrome.storage
前面已经讲过它的选型,这里再补一个更贴近工程落地的理解:
storage.local更像本机扩展配置中心storage.sync更像小体量偏好同步层storage.session更像扩展会话内的短状态区
它常见的工程用法包括:
- 保存用户设置
- 保存最近一次抓取结果
- 在 popup 和 service worker 之间同步状态
- 给 content script 提供配置读取入口
16.4.2 chrome.cookies
它更适合:
- 需要读取站点 Cookie 的扩展
- 需要配合后端会话做辅助判断的场景
要注意的是:
- 它权限更敏感
- 站点边界要配清楚
- 不要把它当成“想读什么 Cookie 都能随便读”
16.4.3 chrome.sessions
它更偏浏览器会话信息,例如:
- 最近关闭的标签页
- 最近关闭的窗口
如果做“恢复工作流”“找回刚关闭页面”这类扩展,它会比较有用。
16.5 action、contextMenus、sidePanel、notifications 是用户入口层
这一组 API 可以看成:扩展和用户碰面的地方。
16.5.1 chrome.action
它主要对应浏览器工具栏上的扩展图标。
常见用途:
- 点击图标打开 popup
- 动态修改图标状态
- 修改 badge 文本和颜色
例如:
- 登录后显示
ON - 当前站点不支持时置灰
- 有新结果时显示计数角标
16.5.2 chrome.contextMenus
它特别适合做:
- 右键菜单增强
- 对选中文本、图片、链接执行动作
- 页面工作流入口
例如“选中文本后翻译”“右键图片后收藏”“右键链接后发送到待办”这类场景,都很适合从这里起步。
16.5.3 chrome.sidePanel
它适合:
- 需要持续可见的工具面板
- 页面助手、AI 助手、分析面板
- 比 popup 更长生命周期的交互界面
如果你的扩展交互比较复杂,side panel 往往比 popup 更自然。
16.5.4 chrome.notifications
它主要用来:
- 提示任务完成
- 提醒错误或异常
- 给后台任务提供轻提醒
但不要滥用,不然很容易把扩展做成打扰源。
16.6 alarms、commands、downloads 更偏调度和工具能力
16.6.1 chrome.alarms
它适合:
- 定时轮询
- 周期清理缓存
- 到点提醒
- 延迟触发后台任务
它的价值在于:service worker 不是常驻的,但定时事件仍然可以靠 alarms 重新唤起处理逻辑。
16.6.2 chrome.commands
它主要解决:
- 给扩展绑定快捷键
- 让高频动作更快触发
例如:
- 快捷打开侧边栏
- 快捷抓取当前页面标题
- 快捷执行页面高亮
16.6.3 chrome.downloads
如果扩展有导出文件、下载报告、保存抓取结果这类需求,这组 API 很常用。
它比“直接让页面下一个文件”更适合由扩展后台统一接管下载行为。
16.7 declarativeNetRequest 是 MV3 下的网络规则主线
以前很多人会把“扩展拦请求”理解成脚本里手动拦截。
但在 MV3 语境下,更需要优先理解的是:
declarativeNetRequest 更像浏览器内部的声明式网络规则系统。
它更适合:
- 请求拦截
- 请求头修改
- URL 重定向
- 广告和追踪规则过滤
它和普通脚本请求的差别在于:
- 不是每次请求都由你手写 JS 即时处理
- 而是把规则声明给浏览器,再由浏览器按规则执行
这样做的好处是:
- 更符合 MV3 约束
- 性能更稳
- 行为边界更清楚
但这一层也要更小心:
- 规则能力更敏感
- 审核更严格
- 权限说明必须更清楚
16.8 一组更实用的 API 组合思路
真正做扩展时,通常不是单独用一个 API,而是几组 API 配合。
比较常见的组合有:
action + tabs + scripting- 用户点击图标
- 找到当前 tab
- 动态注入脚本
contextMenus + tabs + runtime- 用户右键触发动作
- 后台收到菜单点击事件
- 给当前页面或 popup 发消息
runtime + storage + sidePanel- 页面或 popup 把结果发给后台
- 后台存入状态
- side panel 读取并持续展示
alarms + notifications + storage- 定时任务触发
- 后台更新状态
- 再用通知提示用户
tabs + cookies + fetch- 根据当前站点做辅助判断
- 结合登录态或站点上下文访问后端能力
如果把这些组合压缩成一句话,就是:
Chrome Extension API 真正难的不是单个方法怎么调,而是要把入口、后台、页面、状态和权限串成一条稳定主线。
16.9 一个更接近工程场景的小例子
下面这段代码演示的是:
- 安装扩展时注册右键菜单
- 用户选中文本后点击菜单
- 后台读取当前 tab 的选中文本
- 把结果写进
storage - 后续 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: '当前选中文本已经写入扩展存储。'
});
});这段代码真正想说明的是:
- 用户入口可以来自右键菜单,而不一定来自 popup
- 后台脚本可以直接拿到菜单点击信息和 tab 上下文
storage、contextMenus、notifications这几组 API 在工程里经常是连着用的
16.10 学 API 时最容易走偏的地方
最常见的误区有这几个:
- 看到什么都想放进 content script
- 只会调方法,不知道该在哪个上下文里调
- 只会申请权限,不知道为什么需要这些权限
- 把
content_scripts和scripting.executeScript当成完全相同的东西 - 只会让 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这样拆的好处是:
- 一眼能看出不同上下文
- 共享类型和常量有独立位置
- 后续接 TypeScript、React、Vite 时也更容易接
18. 工程里为什么常配合 Vite、React、TypeScript
Chrome Extension 本身不强制你用什么框架。
但现实项目里,下面这套组合很常见:
TypeScriptViteReact或其他 UI 框架
原因不复杂:
- popup、options、side panel 本身就是小型前端页面
- TypeScript 适合管理消息类型、权限配置、存储键名
- Vite 适合组织多入口构建
要注意的是:
扩展是浏览器平台应用,不是普通 SPA。
所以即使用 React/Vite,也不能忘了:
- 仍然有
manifest.json - 仍然有 content script
- 仍然有 service worker
- 仍然有权限模型
19. 调试时最常见的几条线
19.1 调 popup
看 popup 自己的页面控制台。
19.2 调 service worker
去扩展管理页里看后台 service worker 的调试入口。
19.3 调 content script
直接在目标网页 DevTools 里看注入脚本行为。
19.4 调权限和 manifest
很多问题根本不是代码逻辑错,而是:
- 权限没配
- 匹配规则没命中
- 资源没声明
- 修改后没重新加载扩展
🌟 所以扩展调试一定要把“代码问题”和“配置问题”分开排。
20. 发布到 Chrome Web Store 前要检查什么
发布不是把 zip 一传就结束。
至少要检查这些:
- 图标、名称、描述是否完整
- 权限是否最小化
- 是否误申请了过大的站点权限
- 是否包含无关调试代码
- 是否把本地密钥、测试地址、账号信息打包进去了
- 是否有清楚的隐私说明
- 如果涉及远程请求、账号体系、用户数据,要把用途写清楚
真正影响审核通过率的,往往不是 UI,而是:
- 权限理由是否充分
- 数据使用是否透明
- 行为是否和描述一致
21. 安全边界为什么比普通前端更重要
因为扩展天然就更靠近浏览器能力。
真正要特别注意的是:
- 不要随便注入不可信脚本
- 不要把敏感 token 直接暴露给页面上下文
- 不要申请远超实际需求的权限
- 不要把远程执行逻辑做成不透明黑盒
- 对用户数据采集和上传必须说清楚
可以把这条原则记住:
普通网页更多是在自己的站点边界里做事,扩展是在浏览器边界里做事,所以权限和信任问题会更敏感。
22. 最常见的几个坑
22.1 把 service worker 当常驻后台
这会导致:
- 内存状态丢失时一脸懵
- 调试时觉得“刚才还能用,怎么又没了”
22.2 把页面脚本、content script、扩展后台脚本混成同一个世界
这会导致:
- DOM 能拿到,但页面变量拿不到
- 消息链路绕不清
22.3 权限申请过大
这会带来:
- 用户不信任
- 审核更难过
- 安全边界更难控制
22.4 只会写 UI,不理解 manifest 和上下文分工
这样扩展一复杂,马上就会乱。
22.5 只看 API,不看生命周期
扩展很多 bug 不是“不会调 API”,而是:
- 在错误的上下文里调
- 在错误的生命周期节点里调
23. 一份更实用的 Chrome Extension 检查清单
在真正开始做一个扩展前,至少把下面这些问题想清楚:
- 这个能力更适合放在 popup、content script 还是 service worker
- 页面 DOM 操作和后台能力调用的边界是不是清楚
- 需要哪些权限,能不能再缩小
- 需要持久化的数据放哪里
- 消息链路是不是只有一条主线,能不能画出来
- 当前场景到底是页面增强、浏览器能力调用,还是后端协同工具
- UI 是短交互入口还是持续面板
- 调试时该去哪个上下文看日志
- 发布时是否会因为权限和隐私说明被卡住
24. 总结
如果把 Chrome Extension 这条线压缩成最核心的几句话,就是:
- 它是一种运行在浏览器里的前端应用形态
manifest.json决定扩展的入口、权限和运行配置service worker、content script、popup是最核心的三个上下文- 页面 DOM、浏览器 API、用户界面这三条线必须分工清楚
- 权限、通信、存储、生命周期和发布审核,是扩展开发里最容易把项目拉开的地方
真正学 Chrome Extension,不是背一堆 chrome.* API,而是把:
运行结构、上下文边界、权限模型和工程落地方式
这四件事理顺。