Chrome 扩展开发完全指南 / 第 3 章:后台脚本(Service Worker)
第 3 章:后台脚本(Service Worker)
Service Worker 是 Chrome 扩展的"大脑"——它在后台运行,监听浏览器事件,协调各个组件之间的通信。理解它的生命周期和事件模型是构建可靠扩展的关键。
3.1 Service Worker 角色
在 Manifest V3 中,Service Worker 取代了 MV2 的 Background Page 和 Event Page,成为扩展唯一的后台运行机制。
对比 MV2 的后台页面
| 特性 | Background Page (MV2) | Service Worker (MV3) |
|---|---|---|
| 生命周期 | 常驻 / 事件驱逐 | 事件驱动,按需启动 |
| DOM 访问 | ✅ 完整 | ❌ 不可用 |
window 对象 | ✅ 可用 | ❌ 不可用 |
XMLHttpRequest | ✅ 可用 | ❌ 需用 fetch |
setTimeout | ✅ 无限制 | ⚠️ 有限制(~5min) |
| 模块支持 | ❌ 仅 importScripts | ✅ 支持 ES Module |
| 持久化状态 | 内存变量可持久化 | 必须显式持久化 |
3.2 生命周期
Service Worker 的生命周期是理解其行为的关键:
┌─────────┐
│ 安装 │
│installing│
└────┬────┘
│
┌────▼────┐
│ 激活 │
│activating│
└────┬────┘
│
┌───────────────┼───────────────┐
│ │ │
┌────▼────┐ ┌─────▼─────┐ ┌────▼────┐
│ 运行中 │ │ 空闲 │ │ 终止 │
│ running │ │ idle │ │ terminated│
└────┬────┘ └─────┬─────┘ └────┬────┘
│ │ │
└───────────────┴───────────────┘
│
(新事件触发时
重新启动)
各阶段说明
| 阶段 | 触发条件 | 典型操作 |
|---|---|---|
| 安装(Installing) | 首次安装或版本更新 | 初始化数据、注册事件 |
| 激活(Activating) | 安装完成后 | 清理旧版本数据 |
| 运行中(Running) | 有事件正在处理 | 处理事件逻辑 |
| 空闲(Idle) | 所有事件处理完毕 | 等待新事件 |
| 终止(Terminated) | 空闲约 30 秒后 | 被浏览器自动终止 |
⚠️ 注意:Service Worker 在空闲约 30 秒后会被终止。如果有
chrome.alarms或chrome.runtime.onMessage等事件监听器,会在事件触发时重新启动。
3.3 注册与配置
manifest.json 声明
{
"background": {
"service_worker": "background/service-worker.js",
"type": "module"
}
}
type: "module"允许使用 ES Module 的import/export语法- 不设置
type则默认使用 Classic Script,只能用importScripts()
使用 ES Module
// background/service-worker.js
import { fetchData } from './api.js';
import { storage } from '../lib/storage.js';
chrome.runtime.onInstalled.addListener(() => {
console.log('Service Worker loaded as ES Module');
});
// background/api.js
export async function fetchData(url) {
const response = await fetch(url);
return response.json();
}
使用 Classic Script
// Classic Script 方式
importScripts('lib/utils.js', 'lib/storage.js');
chrome.runtime.onInstalled.addListener(() => {
console.log('Service Worker loaded as Classic Script');
});
📌 最佳实践:推荐使用 ES Module(type: "module"),便于代码拆分和复用。
3.4 事件监听
Service Worker 的核心工作是监听和处理事件。以下是最常用的事件类型:
3.4.1 安装与激活事件
// 安装事件 — 首次安装或更新时触发
chrome.runtime.onInstalled.addListener((details) => {
const reason = details.reason;
// reason 可能是:
// 'install' — 首次安装
// 'update' — 版本更新
// 'chrome_update' — Chrome 浏览器更新
// 'shared_module_update' — 共享模块更新
console.log(`Extension ${reason}`);
if (reason === 'install') {
// 首次安装初始化
handleFirstInstall();
} else if (reason === 'update') {
const previousVersion = details.previousVersion;
handleUpdate(previousVersion);
}
});
async function handleFirstInstall() {
// 设置默认配置
await chrome.storage.local.set({
settings: {
theme: 'light',
language: 'zh-CN',
notifications: true,
autoSync: false
},
installDate: Date.now()
});
// 创建上下文菜单
chrome.contextMenus.create({
id: 'mainMenu',
title: '我的扩展',
contexts: ['all']
});
// 设置定时任务
chrome.alarms.create('dailySync', {
periodInMinutes: 1440 // 每 24 小时
});
// 打开欢迎页面
chrome.tabs.create({
url: 'options/welcome.html'
});
}
async function handleUpdate(previousVersion) {
console.log(`从 ${previousVersion} 更新`);
// 数据迁移逻辑
const data = await chrome.storage.local.get(null);
if (data.legacySetting) {
// 迁移旧数据到新格式
await migrateData(data);
}
}
3.4.2 浏览器事件
// 标签页更新事件
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (changeInfo.status === 'complete') {
console.log(`标签页 ${tabId} 加载完成: ${tab.url}`);
}
});
// 标签页激活事件
chrome.tabs.onActivated.addListener((activeInfo) => {
console.log(`切换到标签页 ${activeInfo.tabId}`);
});
// 标签页关闭事件
chrome.tabs.onRemoved.addListener((tabId, removeInfo) => {
console.log(`标签页 ${tabId} 已关闭`);
});
// 窗口焦点变化
chrome.windows.onFocusChanged.addListener((windowId) => {
if (windowId === chrome.windows.WINDOW_ID_NONE) {
console.log('Chrome 窗口失去焦点');
} else {
console.log(`窗口 ${windowId} 获得焦点`);
}
});
// 书签事件
chrome.bookmarks.onCreated.addListener((id, bookmark) => {
console.log(`新书签: ${bookmark.title} - ${bookmark.url}`);
});
// 下载事件
chrome.downloads.onDeterminingFilename.addListener((item, suggest) => {
suggest({ filename: `downloads/${item.filename}` });
return true;
});
3.4.3 定时事件
// 创建定时器
chrome.alarms.create('fetchData', {
delayInMinutes: 1, // 首次触发延迟
periodInMinutes: 30 // 之后每 30 分钟触发
});
chrome.alarms.create('cleanup', {
periodInMinutes: 1440 // 每天执行一次
});
// 监听定时器事件
chrome.alarms.onAlarm.addListener((alarm) => {
switch (alarm.name) {
case 'fetchData':
handleFetchData();
break;
case 'cleanup':
handleCleanup();
break;
case 'dailySync':
handleDailySync();
break;
}
});
async function handleFetchData() {
try {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
await chrome.storage.local.set({ apiData: data });
// 更新徽章
chrome.action.setBadgeText({ text: String(data.count) });
chrome.action.setBadgeBackgroundColor({ color: '#4CAF50' });
} catch (error) {
console.error('数据获取失败:', error);
}
}
3.5 Service Worker 的限制
3.5.1 无 DOM 访问
// ❌ 错误 — Service Worker 中没有 document
const element = document.createElement('div');
// ✅ 正确 — 使用 OffscreenCanvas(用于图像处理)
const canvas = new OffscreenCanvas(200, 200);
const ctx = canvas.getContext('2d');
ctx.fillStyle = 'red';
ctx.fillRect(0, 0, 200, 200);
// ✅ 正确 — 通过消息传递让 Content Script 操作 DOM
chrome.tabs.sendMessage(tabId, { action: 'updateDOM', data: '...' });
3.5.2 状态不持久
// ❌ 错误 — 变量在 Service Worker 重启后丢失
let counter = 0;
// ✅ 正确 — 使用 Storage API 持久化
async function incrementCounter() {
const { counter = 0 } = await chrome.storage.local.get('counter');
await chrome.storage.local.set({ counter: counter + 1 });
return counter + 1;
}
3.5.3 setTimeout 限制
// ⚠️ Service Worker 中 setTimeout 有约 5 分钟的上限
// 超过此时间 Service Worker 可能已被终止
// ❌ 不可靠
setTimeout(() => { /* 可能不会执行 */ }, 600000); // 10 分钟
// ✅ 使用 chrome.alarms 代替
chrome.alarms.create('delayedTask', { delayInMinutes: 10 });
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'delayedTask') {
// 可靠执行
}
});
3.6 Offscreen API
当 Service Worker 需要 DOM 操作(如 HTML 解析、音频播放)时,可以使用 Offscreen API 创建一个离屏文档:
// 创建离屏文档
async function createOffscreenDocument() {
const existingContexts = await chrome.runtime.getContexts({
contextTypes: ['OFFSCREEN_DOCUMENT']
});
if (existingContexts.length > 0) {
return; // 已存在
}
await chrome.offscreen.createDocument({
url: 'offscreen/offscreen.html',
reasons: [
'DOM_PARSER', // 解析 HTML
'WORKERS', // Web Workers
'AUDIO_PLAYBACK', // 音频播放
'BLOBS', // Blob URL 操作
'CANVAS' // Canvas 操作
],
justification: '需要 DOM 操作来解析 HTML 内容'
});
}
// 与离屏文档通信
async function parseHTML(htmlString) {
await createOffscreenDocument();
const response = await chrome.runtime.sendMessage({
type: 'parseHTML',
target: 'offscreen',
html: htmlString
});
return response.result;
}
offscreen/offscreen.html:
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body>
<script src="offscreen.js"></script>
</body>
</html>
// offscreen/offscreen.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.target !== 'offscreen') return;
switch (message.type) {
case 'parseHTML':
const parser = new DOMParser();
const doc = parser.parseFromString(message.html, 'text/html');
const text = doc.body.textContent;
sendResponse({ result: text });
break;
}
});
3.7 数据持久化策略
由于 Service Worker 随时可能被终止,必须采用正确的持久化策略:
推荐方案
// 方案一:使用 chrome.storage(推荐)
async function saveState(key, value) {
await chrome.storage.session.set({ [key]: value });
}
async function getState(key, defaultValue) {
const result = await chrome.storage.session.get(key);
return result[key] ?? defaultValue;
}
// 方案二:使用 IndexedDB(大量数据)
function openDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('extensionDB', 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
db.createObjectStore('cache', { keyPath: 'id' });
};
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
状态管理模式
// 简单的状态管理器
class StateManager {
constructor(storageKey = 'appState') {
this.storageKey = storageKey;
this.state = {};
this.loaded = false;
}
async load() {
const result = await chrome.storage.session.get(this.storageKey);
this.state = result[this.storageKey] || {};
this.loaded = true;
return this.state;
}
async save() {
await chrome.storage.session.set({
[this.storageKey]: this.state
});
}
async get(key, defaultValue) {
if (!this.loaded) await this.load();
return this.state[key] ?? defaultValue;
}
async set(key, value) {
if (!this.loaded) await this.load();
this.state[key] = value;
await this.save();
}
}
// 使用示例
const appState = new StateManager();
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'getState') {
appState.get(message.key).then(value => sendResponse({ value }));
return true;
}
if (message.type === 'setState') {
appState.set(message.key, message.value).then(() => sendResponse({ ok: true }));
return true;
}
});
3.8 业务场景
场景一:定时数据同步
// 每 15 分钟从 API 拉取最新数据
chrome.runtime.onInstalled.addListener(() => {
chrome.alarms.create('syncData', { periodInMinutes: 15 });
});
chrome.alarms.onAlarm.addListener(async (alarm) => {
if (alarm.name !== 'syncData') return;
try {
const { apiUrl } = await chrome.storage.local.get('apiUrl');
const response = await fetch(apiUrl);
const data = await response.json();
await chrome.storage.local.set({
syncData: data,
lastSync: Date.now()
});
chrome.action.setBadgeText({ text: '✓' });
setTimeout(() => chrome.action.setBadgeText({ text: '' }), 3000);
} catch (error) {
chrome.action.setBadgeText({ text: '!' });
chrome.action.setBadgeBackgroundColor({ color: '#FF0000' });
}
});
场景二:拦截新标签页导航
// 将用户从特定页面打开的新标签重定向
chrome.webNavigation.onCreatedNavigationTarget.addListener(
async (details) => {
const tab = await chrome.tabs.get(details.tabId);
if (tab.url?.includes('tracking-site.com')) {
await chrome.tabs.update(details.tabId, {
url: 'https://safe-site.com/redirect?url=' +
encodeURIComponent(details.url)
});
}
}
);