幕后英雄 ------ Background Scripts (Service Worker)
本章目标:理解 Manifest V3 中 Service Worker 的工作模式,学会监听浏览器事件,并为我们的扩展建立一个持久的后台逻辑处理中心。
为什么需要一个"幕后英雄"?
让我们回顾一下我们目前的两个组件:
- Popup:一个华丽的"驾驶舱"。它的生命周期极其短暂,用户点击图标时出现,点击别处时就彻底消失,里面的一切都会被重置。它只适合处理临时的、与用户直接交互的任务。
- Content Script:一支支"外派无人机"。它们与特定网页共存亡,当网页关闭时,它们也就随之消失。它们擅长执行针对特定页面的任务,但无法获得全局视野,也无法在没有网页打开时工作。
现在,请思考几个问题:
- 当用户第一次安装我们的扩展时,我们想弹出一个欢迎页面或者设置一些默认选项,这个逻辑应该由谁来执行?Popup 和 Content Script 显然都不合适。
- 我们想在用户创建了一个新的标签页 或者更新了某个标签页的网址时,立刻得到通知并执行某些操作,这个"监听"任务应该由谁来负责?
- 如果我们的扩展需要**定期(比如每小时)**从服务器获取一些数据,这个定时任务应该放在哪里?
- 当 Popup 需要获取所有标签页的信息时,它向谁去请求?当 Content Script 收集到页面数据后,它又该把数据汇报给谁?谁来做这个**"总调度"**?
所有这些问题的答案,都指向了同一个组件------Background Script(后台脚本)。
在 Manifest V2 的时代,这个后台脚本是一个可以一直存活在后台的页面(Background Page)。但在 Manifest V3 中,为了极大地提升性能和降低资源消耗,Google 将其升级为了一个更现代、更高效的模式------Service Worker。
你可以把 Service Worker 想象成我们扩展的**"全天候待命的事件处理中心"。它平时处于"休眠"状态,不占用任何系统资源。但一旦有它所关心的事件**发生(比如扩展被安装、用户点击了某个菜单、闹钟响起),浏览器就会瞬间唤醒它,让它处理这个事件。处理完毕后,如果短时间内没有新的事件,它会再次进入休眠。
它就是那个在幕后默默守护着一切,不求闻达,却又不可或缺的英雄。
4.1 认识 MV3 的新核心:Service Worker
在深入实践之前,我们必须花一点时间来理解 Service Worker 的几个关键特性,因为它的工作模式与我们之前接触的任何 JavaScript 环境都大相径庭。
1. 事件驱动 (Event-driven)
这是你必须刻在脑子里的第一条准则。Service Worker 的所有代码,都应该被包裹在事件监听器中。
你不能在 Service Worker 的顶层作用域(全局作用域)编写像 setInterval
或者 fetch
这样的持续性或异步任务。因为当 Service Worker 执行完顶层代码并进入休眠后,这些任务就会被强行终止。
正确的做法是,把你的逻辑注册为对特定事件的响应。
错误的做法 ❌:
javascript
// background.js (顶层作用域)
let counter = 0;
setInterval(() => {
counter++;
console.log("计时器:", counter); // 这个计时器会在Service Worker休眠后被杀死
}, 1000);
正确的做法 ✅:
javascript
// background.js
// 监听扩展首次安装事件
chrome.runtime.onInstalled.addListener(() => {
// 只有当 onInstalled 事件发生时,这里的代码才会被执行
console.log("扩展已安装!");
chrome.storage.local.set({ enabled: true });
});
// 监听闹钟事件 (用于替代 setInterval)
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'my-periodic-task') {
console.log("闹钟响了,执行定时任务!");
}
});
在正确的例子中,代码只有在特定的事件(onInstalled
, onAlarm
)被触发时才会运行。
2. 非持久性 (Non-persistent)
Service Worker 是一个"召之即来,挥之即去"的角色。浏览器为了节省资源,会非常"积极"地让它休眠。
- 唤醒:当一个它监听的事件发生时,它会被唤醒。
- 运行 :它有大约 30 秒的时间来处理这个事件。如果你的异步操作(比如一个
fetch
请求)超过 30 秒还没完成,它可能会被强行终止。 - 休眠:事件处理完毕后,如果短时间内没有新事件,它就会再次进入休眠状态。
这个特性带来的直接后果是:你绝对不能依赖 Service Worker 中的全局变量来存储状态!
错误的做法 ❌:
javascript
// background.js
let userSettings = null; // 这是一个全局变量
chrome.runtime.onInstalled.addListener(() => {
userSettings = { theme: 'dark' }; // 在安装时设置
console.log("设置已初始化:", userSettings);
});
chrome.action.onClicked.addListener(() => {
// 当用户点击工具栏图标时...
// 这里的 userSettings 很有可能已经是 null 了!
// 因为从安装到用户点击,Service Worker 可能已经休眠并重启了无数次,
// 全局变量早就被重置了。
if (userSettings && userSettings.theme === 'dark') {
console.log("执行暗色主题逻辑");
}
});
正确的做法 ✅:
使用 chrome.storage
API。这是一个专为扩展设计的、持久化的存储方案。它独立于 Service Worker 的生命周期。
javascript
// background.js
chrome.runtime.onInstalled.addListener(() => {
// 将状态存储在持久化的 chrome.storage 中
chrome.storage.local.set({ theme: 'dark' });
console.log("设置已存储到 storage");
});
chrome.action.onClicked.addListener(async () => {
// 每次需要状态时,都从 storage 中异步获取
const data = await chrome.storage.local.get('theme');
const userSettings = { theme: data.theme };
if (userSettings.theme === 'dark') {
console.log("成功获取到状态,执行暗色主题逻辑");
}
});
在正确的例子中,我们将状态保存在了 chrome.storage
这个"外部保险箱"里。无论 Service Worker 重启多少次,保险箱里的东西都不会丢。
3. 如何调试 Service Worker
这是另一个核心技巧。既然 Service Worker 在后台运行,没有界面,那我们去哪里看它的 console.log
和错误呢?
答案就在我们的老朋友------扩展管理页面 (chrome://extensions
)。
- 找到你的扩展卡片。
- 在 Manifest V3 中,你会看到一个名为 "Service Worker" 或 "查看视图:Service Worker" 的蓝色链接。
- 点击这个链接!
一个专门用于调试你的 Service Worker 的开发者工具窗口就会被打开。你可以在它的 Console
面板看到所有后台日志,在 Sources
面板打断点调试,在 Application
面板查看 Storage 的状态。
重要提示 :这个调试窗口只要开着,浏览器就会为了方便你调试,而强制让你的 Service Worker 保持唤醒状态。这在开发时非常方便,但要记住,在测试真实休眠行为时,需要把这个窗口关掉。
4.2 项目实战:搭建我们的"指挥中心"
理论学习结束,让我们撸起袖子,为我们的"智能标签页管家"正式启用它的"大脑"。
第一步:创建 Background Script 文件
和 content.js
一样,我们把后台脚本也放在 scripts
文件夹里。
在 my-first-extension/scripts/
目录下,创建一个新文件,命名为 background.js
。
📂 my-first-extension/
└── 📂 scripts/
├── 📄 background.js <-- 新建这个文件
└── 📄 content.js
第二步:在 manifest.json
中注册 Service Worker
我们需要再次修改我们的"营业执照",告诉浏览器:"嘿,我有一个后台 Service Worker,它的文件在这里,请你帮我管理它。"
打开 manifest.json
,在顶级作用域添加一个新的字段:"background"
。
json
{
"manifest_version": 3,
"name": "我的第一个扩展",
"version": "1.0.0",
...
"action": { ... },
"background": {
"service_worker": "scripts/background.js"
},
"content_scripts": [ ... ]
}
这个配置非常简单:
"background"
: 声明后台脚本配置的对象。"service_worker": "scripts/background.js"
: 指定我们的 Service Worker 脚本的路径。路径同样是相对于扩展根目录的。
在 MV3 中,后台脚本必须通过这种方式指定,而且只能有一个。
第三步:编写我们的第一个后台逻辑
现在,让我们在 background.js
里写入一些初始的、有代表性的事件监听逻辑。
打开 scripts/background.js
,输入以下代码:
javascript
// scripts/background.js
// --- 1. 监听扩展安装和更新事件 ---
chrome.runtime.onInstalled.addListener((details) => {
// 打印事件对象,方便调试
console.log("onInstalled event details:", details);
// 判断事件类型
if (details.reason === "install") {
console.log("感谢安装!这是您的第一次使用。");
// 第一次安装时,可以执行一些初始化操作
// 比如,设置默认的存储值
chrome.storage.local.set({
isExtensionEnabled: true,
blockedSites: ["www.example.com"]
});
// 第一次安装后,可以打开一个欢迎页面或者教程页面
// chrome.tabs.create({
// url: "welcome.html" // 我们需要先创建这个页面
// });
} else if (details.reason === "update") {
const previousVersion = details.previousVersion;
console.log(`扩展已从版本 ${previousVersion} 更新!`);
// 在这里可以处理版本更新的逻辑,比如迁移旧数据
}
});
// --- 2. 监听工具栏图标点击事件 ---
// 注意:如果 manifest.json 中定义了 popup 页面,
// 这个 onClicked 事件将不会被触发,因为点击的默认行为是打开 popup。
// 我们在这里写出来,是为了演示这个重要的 API。
chrome.action.onClicked.addListener((tab) => {
console.log("工具栏图标被点击了!");
console.log("当前标签页信息:", tab);
// 在没有popup时,可以用来执行一些快捷操作
});
// --- 3. 监听标签页更新事件 ---
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
// changeInfo 对象包含了变化的具体信息,比如 status 或 url
// status 通常会经历 "loading" -> "complete" 的变化
if (changeInfo.status === 'complete' && tab.url) {
console.log(`标签页 ${tabId} 已加载完成, URL: ${tab.url}`);
// 在这里,我们可以根据 tab.url 执行一些逻辑
// 比如,如果 URL 匹配了某个规则,就禁用我们的 browserAction
if (tab.url.includes("google.com")) {
// chrome.action.disable(tabId);
// console.log(`在 Google 页面禁用了图标`);
} else {
// chrome.action.enable(tabId);
}
}
});
// --- 4. 监听一个简单的消息 (为后续章节做准备) ---
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
console.log("收到了来自 Content Script 或 Popup 的消息:", message);
console.log("消息发送方信息:", sender);
if (message.greeting === "hello from content script") {
// 收到消息后,可以做一些处理,然后回复
sendResponse({ farewell: "你好,Content Script,我已经收到你的消息了。" });
}
// `return true;` 在需要异步发送响应时使用,我们后面会详细讲
});
console.log("Background Service Worker 已启动并正在监听事件。");
这是一段信息量巨大的代码,但别怕,它清晰地展示了 Service Worker 的核心工作模式------"监听-响应"。让我们逐一分析这些"哨兵":
-
chrome.runtime.onInstalled
:- 何时触发:在扩展被新安装、更新到一个新版本,或者浏览器更新时触发。
- 作用 :这是执行一次性初始化 操作的绝佳位置。比如设置默认值到
chrome.storage
,或者像注释里写的,打开一个欢迎页面。 details.reason
: 这个参数非常有用,它可以告诉你这次事件是"install"
(新安装)还是"update"
(更新),让你能执行不同的逻辑。
-
chrome.action.onClicked
:- 何时触发:当用户点击工具栏上的扩展图标时。
- 重要前提 :只有在
manifest.json
的action
字段里没有定义default_popup
的情况下,这个事件才会生效! 如果定义了 popup,点击的默认行为就是打开 popup,这个事件监听器就会被忽略。 - 作用:对于那些不需要复杂 UI,只需要一个快捷开关的扩展来说,这是核心交互。比如一个一键切换网页黑白模式的扩展。
-
chrome.tabs.onUpdated
:- 何时触发 :当一个标签页被更新时。这个事件会触发多次,比如 URL 改变、加载状态改变(从
loading
到complete
)。 - 作用:这是一个非常强大的"嗅探器"。你可以实时监控用户的浏览行为。
changeInfo.status === 'complete'
: 我们通常会加上这个判断,确保只在页面加载完成时才执行逻辑,避免不必要的重复操作。- 在注释的代码中,我们演示了如何根据 URL 动态地禁用 (disable) 或启用 (enable) 我们的工具栏图标,让它在某些网站上变成灰色不可点击状态。这是一种非常常见的交互优化。
- 何时触发 :当一个标签页被更新时。这个事件会触发多次,比如 URL 改变、加载状态改变(从
-
chrome.runtime.onMessage
:- 何时触发 :当扩展的其他部分(如 Content Script 或 Popup)使用
chrome.runtime.sendMessage
发送消息时。 - 作用 :这是我们扩展内部通信的核心枢纽。它就像是"指挥中心"的总接线员。我们会在下一章详细讲解它。现在,我们先把它放在这里,让它准备好接收信号。
- 何时触发 :当扩展的其他部分(如 Content Script 或 Popup)使用
第四步:申请权限
你可能已经注意到了,在上面的代码里,我们用到了 chrome.storage
和 chrome.tabs
这些 API。默认情况下,我们的扩展是没有权限使用这些强大功能的。
如果我们不申请权限就直接使用,浏览器会在 Service Worker 的控制台里无情地报错:"Uncaught TypeError: Cannot read properties of undefined (reading 'local')
" 或者 "Access to 'tabs' is not allowed
"。
所以,我们必须再次更新我们的"营业执照" manifest.json
,明确地告诉浏览器:"我需要使用'存储'和'标签页'这两项特权!"
在 manifest.json
的顶级作用域,添加一个新的字段:"permissions"
。
json
{
"manifest_version": 3,
...
"background": {
"service_worker": "scripts/background.js"
},
"permissions": [
"storage",
"tabs"
],
"content_scripts": [ ... ]
}
"permissions"
: 它的值是一个数组,里面列出了你扩展需要的所有权限。"storage"
: 允许我们使用chrome.storage
API。"tabs"
: 允许我们使用chrome.tabs
API(比如onUpdated
,create
,query
等)。
权限申请原则:按需、最少。 永远只申请你功能所必需的权限。申请过多的权限不仅可能在应用商店审核时遇到麻烦,也会引起用户的警惕。
第五步:部署与验证
现在,我们已经万事俱备了。
- 保存 所有修改过的文件 (
manifest.json
,background.js
)。 - 回到
chrome://extensions
页面,刷新 我们的扩展。- 第一次刷新(模拟安装) :因为你添加了
background
字段,这相当于一次重大更新。刷新后,你应该能看到你的扩展卡片上多出了那个蓝色的 "Service Worker" 链接。
- 第一次刷新(模拟安装) :因为你添加了
- 点击"Service Worker"链接 ,打开后台的开发者工具。
- 在
Console
面板,你应该能看到onInstalled
事件被触发时打印的日志,比如 "扩展已从版本 xxx 更新!"。 - 你还能看到最下面那句:"Background Service Worker 已启动并正在监听事件。"
- 在
- 验证
onUpdated
事件 :- 保持后台开发者工具窗口打开。
- 在你的浏览器里随便打开一些新的标签页,或者刷新现有的标签页。
- 观察后台的
Console
,你会看到onUpdated
事件被频繁触发,并在页面加载完成时打印出 "标签页 xxx 已加载完成, URL: ..." 的信息。
- 验证
storage
初始化 :- 在后台开发者工具窗口中,切换到
Application
面板。 - 在左侧的
Storage
->Local Storage
下,虽然你看不到chrome.storage.local
的直接内容,但你可以通过在 Console 中执行chrome.storage.local.get(console.log)
来查看。你应该能看到我们初始化的{ isExtensionEnabled: true, blockedSites: [...] }
。
- 在后台开发者工具窗口中,切换到
到此为止,我们已经成功地为我们的扩展植入了"大脑"。它现在是一个拥有后台逻辑、能够响应浏览器级事件、并为未来通信做好了准备的、更加完整的应用程序了。
本章总结与展望
在这一章,我们攻克了 Manifest V3 中最核心、最具变革性的一个概念:
- 理解了 Service Worker :我们掌握了它"事件驱动"、"非持久性"的核心工作模式,并知道了不能依赖全局变量,而应使用
chrome.storage
来持久化状态。 - 学会了调试后台 :我们知道了如何通过
chrome://extensions
页面打开 Service Worker 的专属开发者工具。 - 实践了事件监听:我们亲手编写了对扩展安装、标签页更新等关键浏览器事件的监听器,让我们的扩展具备了后台处理能力。
- 掌握了权限申请 :我们学会了在
manifest.json
中使用permissions
字段,为我们的扩展申请必要的操作权限。
我们的"智能标签页管家"现在拥有了:
- 一个漂亮的交互界面 (Popup)
- 一支强大的勘探部队 (Content Scripts)
- 一个可靠的指挥中心 (Background Script)
所有的部件都已就位。但是,它们之间还是一座座"孤岛"。指挥中心无法向勘探部队下达指令,勘探部队也无法将情报传回,交互界面更是无法从指挥中心获取全局数据。
在下一章,我们将要搭建连接这些孤岛的"跨海大桥"------我们将深入学习扩展内部的消息通信机制 (Messaging)。
那将是真正激动人心的时刻,我们将把所有独立的模块连接成一个有机的、协同工作的整体。我们的"智能标签页管家"将真正开始变得"智能"。