第四章:幕后英雄 —— Background Scripts (Service Worker)

第四章:幕后英雄 ------ Background Scripts (Service Worker)

本章目标:理解 Manifest V3 中 Service Worker 的工作模式,学会监听浏览器事件,并为我们的扩展建立一个持久的后台逻辑处理中心。


为什么需要一个"幕后英雄"?

让我们回顾一下我们目前的两个组件:

  • Popup:一个华丽的"驾驶舱"。它的生命周期极其短暂,用户点击图标时出现,点击别处时就彻底消失,里面的一切都会被重置。它只适合处理临时的、与用户直接交互的任务。
  • Content Script:一支支"外派无人机"。它们与特定网页共存亡,当网页关闭时,它们也就随之消失。它们擅长执行针对特定页面的任务,但无法获得全局视野,也无法在没有网页打开时工作。

现在,请思考几个问题:

  1. 当用户第一次安装我们的扩展时,我们想弹出一个欢迎页面或者设置一些默认选项,这个逻辑应该由谁来执行?Popup 和 Content Script 显然都不合适。
  2. 我们想在用户创建了一个新的标签页 或者更新了某个标签页的网址时,立刻得到通知并执行某些操作,这个"监听"任务应该由谁来负责?
  3. 如果我们的扩展需要**定期(比如每小时)**从服务器获取一些数据,这个定时任务应该放在哪里?
  4. 当 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)。

  1. 找到你的扩展卡片。
  2. 在 Manifest V3 中,你会看到一个名为 "Service Worker""查看视图:Service Worker" 的蓝色链接。
  3. 点击这个链接!

一个专门用于调试你的 Service Worker 的开发者工具窗口就会被打开。你可以在它的 Console 面板看到所有后台日志,在 Sources 面板打断点调试,在 Application 面板查看 Storage 的状态。

重要提示 :这个调试窗口只要开着,浏览器就会为了方便你调试,而强制让你的 Service Worker 保持唤醒状态。这在开发时非常方便,但要记住,在测试真实休眠行为时,需要把这个窗口关掉。


4.2 项目实战:搭建我们的"指挥中心"

理论学习结束,让我们撸起袖子,为我们的"智能标签页管家"正式启用它的"大脑"。

第一步:创建 Background Script 文件

content.js 一样,我们把后台脚本也放在 scripts 文件夹里。

my-first-extension/scripts/ 目录下,创建一个新文件,命名为 background.js

css 复制代码
📂 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 的核心工作模式------"监听-响应"。让我们逐一分析这些"哨兵":

  1. chrome.runtime.onInstalled:

    • 何时触发:在扩展被新安装、更新到一个新版本,或者浏览器更新时触发。
    • 作用 :这是执行一次性初始化 操作的绝佳位置。比如设置默认值到 chrome.storage,或者像注释里写的,打开一个欢迎页面。
    • details.reason: 这个参数非常有用,它可以告诉你这次事件是 "install"(新安装)还是 "update"(更新),让你能执行不同的逻辑。
  2. chrome.action.onClicked:

    • 何时触发:当用户点击工具栏上的扩展图标时。
    • 重要前提只有在 manifest.jsonaction 字段里没有定义 default_popup 的情况下,这个事件才会生效! 如果定义了 popup,点击的默认行为就是打开 popup,这个事件监听器就会被忽略。
    • 作用:对于那些不需要复杂 UI,只需要一个快捷开关的扩展来说,这是核心交互。比如一个一键切换网页黑白模式的扩展。
  3. chrome.tabs.onUpdated:

    • 何时触发 :当一个标签页被更新时。这个事件会触发多次,比如 URL 改变、加载状态改变(从 loadingcomplete)。
    • 作用:这是一个非常强大的"嗅探器"。你可以实时监控用户的浏览行为。
    • changeInfo.status === 'complete': 我们通常会加上这个判断,确保只在页面加载完成时才执行逻辑,避免不必要的重复操作。
    • 在注释的代码中,我们演示了如何根据 URL 动态地禁用 (disable)启用 (enable) 我们的工具栏图标,让它在某些网站上变成灰色不可点击状态。这是一种非常常见的交互优化。
  4. chrome.runtime.onMessage:

    • 何时触发 :当扩展的其他部分(如 Content Script 或 Popup)使用 chrome.runtime.sendMessage 发送消息时。
    • 作用 :这是我们扩展内部通信的核心枢纽。它就像是"指挥中心"的总接线员。我们会在下一章详细讲解它。现在,我们先把它放在这里,让它准备好接收信号。

第四步:申请权限

你可能已经注意到了,在上面的代码里,我们用到了 chrome.storagechrome.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 等)。

权限申请原则:按需、最少。 永远只申请你功能所必需的权限。申请过多的权限不仅可能在应用商店审核时遇到麻烦,也会引起用户的警惕。

第五步:部署与验证

现在,我们已经万事俱备了。

  1. 保存 所有修改过的文件 (manifest.json, background.js)。
  2. 回到 chrome://extensions 页面,刷新 我们的扩展。
    • 第一次刷新(模拟安装) :因为你添加了 background 字段,这相当于一次重大更新。刷新后,你应该能看到你的扩展卡片上多出了那个蓝色的 "Service Worker" 链接。
  3. 点击"Service Worker"链接 ,打开后台的开发者工具。
    • Console 面板,你应该能看到 onInstalled 事件被触发时打印的日志,比如 "扩展已从版本 xxx 更新!"。
    • 你还能看到最下面那句:"Background Service Worker 已启动并正在监听事件。"
  4. 验证 onUpdated 事件
    • 保持后台开发者工具窗口打开。
    • 在你的浏览器里随便打开一些新的标签页,或者刷新现有的标签页。
    • 观察后台的 Console,你会看到 onUpdated 事件被频繁触发,并在页面加载完成时打印出 "标签页 xxx 已加载完成, URL: ..." 的信息。
  5. 验证 storage 初始化
    • 在后台开发者工具窗口中,切换到 Application 面板。
    • 在左侧的 Storage -> Local Storage 下,虽然你看不到 chrome.storage.local 的直接内容,但你可以通过在 Console 中执行 chrome.storage.local.get(console.log) 来查看。你应该能看到我们初始化的 { isExtensionEnabled: true, blockedSites: [...] }

到此为止,我们已经成功地为我们的扩展植入了"大脑"。它现在是一个拥有后台逻辑、能够响应浏览器级事件、并为未来通信做好了准备的、更加完整的应用程序了。


本章总结与展望

在这一章,我们攻克了 Manifest V3 中最核心、最具变革性的一个概念:

  1. 理解了 Service Worker :我们掌握了它"事件驱动"、"非持久性"的核心工作模式,并知道了不能依赖全局变量,而应使用 chrome.storage 来持久化状态。
  2. 学会了调试后台 :我们知道了如何通过 chrome://extensions 页面打开 Service Worker 的专属开发者工具。
  3. 实践了事件监听:我们亲手编写了对扩展安装、标签页更新等关键浏览器事件的监听器,让我们的扩展具备了后台处理能力。
  4. 掌握了权限申请 :我们学会了在 manifest.json 中使用 permissions 字段,为我们的扩展申请必要的操作权限。

我们的"智能标签页管家"现在拥有了:

  • 一个漂亮的交互界面 (Popup)
  • 一支强大的勘探部队 (Content Scripts)
  • 一个可靠的指挥中心 (Background Script)

所有的部件都已就位。但是,它们之间还是一座座"孤岛"。指挥中心无法向勘探部队下达指令,勘探部队也无法将情报传回,交互界面更是无法从指挥中心获取全局数据。

在下一章,我们将要搭建连接这些孤岛的"跨海大桥"------我们将深入学习扩展内部的消息通信机制 (Messaging)。

那将是真正激动人心的时刻,我们将把所有独立的模块连接成一个有机的、协同工作的整体。我们的"智能标签页管家"将真正开始变得"智能"。

相关推荐
掘金安东尼7 分钟前
Rspack 推出 Rslint:一个用 Go 编写的 TypeScript-First Linter
前端·javascript·github
一枚前端小能手7 分钟前
正则~~~来看这里
前端·正则表达式
你听得到1111 分钟前
弹窗库1.1.0版本发布!不止于统一,更是全面的体验升级!
android·前端·flutter
RaidenLiu12 分钟前
Riverpod 3 :掌握异步任务处理与 AsyncNotifier
前端·flutter
前端付豪16 分钟前
🔥Vue3 Composition API 核心特性深度解析:为什么说它是前端的“终极武器”?
前端·vue.js
skeletron201126 分钟前
【基础】React工程配置(基于Vite配置)
前端
怪可爱的地球人27 分钟前
前端
蓝胖子的小叮当35 分钟前
JavaScript基础(十四)字符串方法总结
前端·javascript
跟橙姐学代码1 小时前
Python 函数实战手册:学会这招,代码能省一半!
前端·python·ipython
森之鸟1 小时前
审核问题——鸿蒙审核返回安装失败,可以尝试云调试
服务器·前端·数据库