强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 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.alarmschrome.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)
      });
    }
  }
);

3.9 扩展阅读