幕后英雄 —— 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

复制代码
📂 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)。

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

相关推荐
盏灯1 分钟前
🔐🔐🔐 数据库大表,加字段,卡死导致损失惨重!
前端
青红光硫化黑1 分钟前
学习bug
开发语言·javascript·ecmascript
拾光拾趣录14 分钟前
🔥9种继承写法全解,第7种99%人没用过?⚠️
前端·面试
李梦晓18 分钟前
git 提交代码到别的分支
前端·git
LIUENG20 分钟前
Vue2 中的响应式原理
前端·vue.js
陈随易20 分钟前
VSCode v1.103发布,AI编程任务列表,可用GPT 5和Claude 4.1
前端·后端·程序员
电商数据girl33 分钟前
关于私域电商网站,接入电商API数据接口示例
运维·开发语言·网络·python·json·php
视觉CG36 分钟前
【JS】扁平树数据转为树结构
android·java·javascript
wordbaby40 分钟前
以0deg为起点,探讨CSS线性渐变的方向
前端·css
猩猩程序员42 分钟前
宣布 Rust 1.89.0 发布
前端