迁移到 Service Worker

使用 Service Worker 替换背景或事件页面

Service Worker 会替换扩展程序的后台或事件页面,以确保后台代码不会占用主线程。这样,扩展程序便可在需要时运行,从而节省资源。

自扩展程序推出以来,后台网页一直是其基本组成部分。简单来说,后台网页提供了一个独立于任何其他窗口或标签页的环境。这样,扩展程序就可以观察事件并采取相应行动。

本页面介绍了将后台页面转换为扩展程序服务工作线程的任务。如需详细了解扩展程序服务工作线程,请参阅教程使用服务工作线程处理事件和关于扩展程序服务工作线程部分。

后台脚本与扩展程序服务工作线程之间的区别

在某些情况下,您会看到扩展程序 Service Worker 被称为“后台脚本”。虽然扩展程序 Service Worker 在后台运行,但将其称为后台脚本会产生误导,暗示它们具有相同的功能。区别将在下面进行介绍。

来自后台网页的更改

Service Worker 与后台网页有许多不同之处。

  • 它们在主线程之外运行,这意味着它们不会干扰扩展程序内容。
  • 它们具有特殊功能,例如拦截扩展程序来源上的提取事件(例如来自工具栏弹出式窗口的提取事件)。
  • 它们可以通过 Clients 接口与其他上下文进行通信和互动。

您需要做出的更改

您需要对代码进行一些调整,以应对后台脚本和服务工作线程之间的功能差异。首先,在清单文件中指定 Service Worker 的方式与指定后台脚本的方式不同。此外:

  • 由于它们无法访问 DOM 或 window 接口,因此您需要将此类调用移至其他 API 或移入屏幕外文档。
  • 不应注册事件监听器以响应返回的 promise 或在事件回调中注册事件监听器。
  • 由于它们与 XMLHttpRequest() 不向后兼容,因此您需要将对该接口的调用替换为对 fetch() 的调用。
  • 由于它们在不使用时会终止,因此您需要持久保存应用状态,而不是依赖全局变量。终止服务工作线程也会在计时器完成之前结束计时。您需要将它们替换为闹钟。

本页详细介绍了这些任务。

更新清单中的“background”字段

在 Manifest V3 中,后台网页由 Service Worker 取代。清单变更如下所示。

  • 将 manifest.json 中的 "background.scripts" 替换为 "background.service_worker"。请注意,"service_worker" 字段接受的是字符串,而不是字符串数组。
  • 从 manifest.json 中移除了 "background.persistent"。
Manifest V2
{
  ...
  "background": {
    "scripts": [
      "backgroundContextMenus.js",
      "backgroundOauth.js"
    ],
    "persistent": false
  },
  ...
}
Manifest V3
{
  ...
  "background": {
    "service_worker": "service_worker.js",
    "type": "module"
  }
  ...
}

"service_worker" 字段接受单个字符串。只有在使用 ES 模块(使用 import 关键字)时,才需要 "type" 字段。其值始终为 "module"。如需了解详情,请参阅扩展 Service Worker 基础知识

将 DOM 和窗口调用移至屏幕外文档

有些扩展程序需要访问 DOM 和窗口对象,但不能以直观方式打开新窗口或标签页。Offscreen API 通过打开和关闭与扩展程序打包在一起的未显示文档来支持这些使用情形,而不会影响用户体验。除了消息传递之外,屏幕外文档不与其他扩展程序上下文共享 API,但可以作为完整的网页供扩展程序与之互动。

如需使用 Offscreen API,请通过 Service Worker 创建屏幕外文档。

browser.offscreen.createDocument({
  url: browser.runtime.getURL('offscreen.html'),
  reasons: ['CLIPBOARD'],
  justification: 'testing the offscreen API',
});

在屏幕外文档中,执行您之前会在后台脚本中运行的任何操作。例如,您可以复制在宿主网页上选择的文本。

let textEl = document.querySelector('#text');
textEl.value = data;
textEl.select();
document.execCommand('copy');

使用消息传递在屏幕外文档和扩展程序服务工作器之间进行通信。

将 localStorage 转换为其他类型

Web 平台的 Storage 接口(可从 window.localStorage 访问)无法在 Service Worker 中使用。若要解决此问题,请执行以下两项操作之一。首先,您可以将其替换为对其他存储机制的调用。browser.storage.local 命名空间可满足大多数使用情形,但您也可以选择其他选项。

您还可以将其调用移至屏幕外文档。例如,如需将之前存储在 localStorage 中的数据迁移到其他机制,请执行以下操作:

  1. 创建具有转换例程和 runtime.onMessage 处理程序的屏幕外文档。
  2. 向屏幕外文档添加了转换例程。
  3. 在扩展程序 Service Worker 中,检查您的数据的 browser.storage。
  4. 如果未找到数据,请创建一个屏幕外文档,并调用 runtime.sendMessage() 以启动转换例程。
  5. 在您添加到屏幕外文档中的 runtime.onMessage 处理程序中,调用转换例程。

扩展程序中的 Web 存储 API 在使用方式上也有一些细微差别。如需了解详情,请参阅存储空间和 Cookie。

同步注册监听器

在 Manifest V3 中,异步注册监听器(例如在 Promise 或回调中)不保证能正常运行。请看以下代码。

browser.storage.local.get(["badgeText"], ({ badgeText }) => {
  browser.browserAction.setBadgeText({ text: badgeText });
  browser.browserAction.onClicked.addListener(handleActionClick);
});

这适用于持久性后台网页,因为该网页会持续运行,且永远不会重新初始化。在 Manifest V3 中,当调度事件时,Service Worker 将重新初始化。这意味着,当事件触发时,监听器将不会注册(因为它们是异步添加的),并且事件将被错过。

请改为将事件监听器注册移至脚本的顶层。这样可确保 Chrome 能够立即找到并调用操作的点击处理程序,即使扩展程序尚未完成其启动逻辑的执行也是如此。

browser.action.onClicked.addListener(handleActionClick);

browser.storage.local.get(["badgeText"], ({ badgeText }) => {
  browser.action.setBadgeText({ text: badgeText });
});

将 XMLHttpRequest() 替换为全局 fetch()

无法从 Service Worker、扩展程序或其他位置调用 XMLHttpRequest()。将后台脚本中对 XMLHttpRequest() 的调用替换为对全局 fetch() 的调用。

XMLHttpRequest()
const xhr = new XMLHttpRequest();
console.log('UNSENT', xhr.readyState);

xhr.open('GET', '/api', true);
console.log('OPENED', xhr.readyState);

xhr.onload = () => {
    console.log('DONE', xhr.readyState);
};
xhr.send(null);
fetch()
const response = await fetch('https://www.example.com/greeting.json'')
console.log(response.statusText);

持久状态

服务工作线程是短暂的,这意味着它们很可能会在用户浏览器的会话期间反复启动、运行和终止。这也意味着,由于之前的上下文已被拆除,因此数据不会立即在全局变量中提供。为了规避此问题,请使用存储 API 作为可信来源。以下示例将展示如何执行此操作。

以下示例使用全局变量来存储名称。在 service worker 中,此变量可能会在用户浏览器会话期间重置多次。

Manifest V2 后台脚本
let savedName = undefined;

browser.runtime.onMessage.addListener(({ type, name }) => {
  if (type === "set-name") {
    savedName = name;
  }
});

browser.browserAction.onClicked.addListener((tab) => {
  browser.tabs.sendMessage(tab.id, { name: savedName });
});

对于 Manifest V3,请将全局变量替换为对 Storage API 的调用。

Manifest V3 Service Worker
browser.runtime.onMessage.addListener(({ type, name }) => {
  if (type === "set-name") {
    browser.storage.local.set({ name });
  }
});

browser.action.onClicked.addListener(async (tab) => {
  const { name } = await browser.storage.local.get(["name"]);
  browser.tabs.sendMessage(tab.id, { name });
});

将定时器转换为闹钟

通常会使用 setTimeout() 或 setInterval() 方法来执行延迟或定期操作。不过,这些 API 在 service worker 中可能会失败,因为每当 service worker 终止时,计时器都会被取消。

Manifest V2 后台脚本
// 3 minutes in milliseconds
const TIMEOUT = 3 * 60 * 1000;
setTimeout(() => {
  browser.action.setIcon({
    path: getRandomIconPath(),
  });
}, TIMEOUT);

请改用 Alarms API。与其他监听器一样,闹钟监听器应在脚本的顶层注册。

Manifest V3 Service Worker
async function startAlarm(name, duration) {
  await browser.alarms.create(name, { delayInMinutes: 3 });
}

browser.alarms.onAlarm.addListener(() => {
  browser.action.setIcon({
    path: getRandomIconPath(),
  });
});

保持 Service Worker 处于活动状态

从定义上来说,Service Worker 是事件驱动型的,并且会在不活动时终止。这样,Chrome 就可以优化扩展程序的性能和内存消耗。如需了解详情,请参阅我们的 Service Worker 生命周期文档。在特殊情况下,可能需要采取额外措施来确保 Service Worker 保持更长时间的活跃状态。

使 Service Worker 保持活跃状态,直到长时间运行的操作完成

在不调用扩展程序 API 的长时间运行的服务工作线程操作期间,服务工作线程可能会在操作中途关闭。例如:

  • 可能需要 5 分钟以上时间的 fetch() 请求(例如,在网络连接可能较差的情况下进行大型下载)。
  • 需要 30 秒以上的复杂异步计算。

如需在这些情况下延长 Service Worker 的生命周期,您可以定期调用一个微不足道的扩展 API 来重置超时计数器。请注意,此方法仅适用于特殊情况,在大多数情况下,通常有更好的平台惯用方法可以实现相同的结果。

以下示例展示了一个 waitUntil() 辅助函数,该函数可让您的 service worker 在给定的 promise 解析之前保持活跃状态:

async function waitUntil(promise) {
  const keepAlive = setInterval(browser.runtime.getPlatformInfo, 25 * 1000);
  try {
    await promise;
  } finally {
    clearInterval(keepAlive);
  }
}

waitUntil(someExpensiveCalculation());

使 Service Worker 持续保持活跃状态

在极少数情况下,需要无限期延长生命周期。我们已确定企业和教育是最大的应用场景,因此专门允许在这些场景中使用,但一般情况下不支持。在这些特殊情况下,可以通过定期调用一个微不足道的扩展程序 API 来保持 Service Worker 处于活跃状态。请务必注意,此建议仅适用于在受管设备上运行的扩展程序,这些设备用于企业或教育用例。在其他情况下,不允许使用此 API,Chrome 扩展程序团队保留在未来针对此类扩展程序采取措施的权利。

使用以下代码段可让您的 Service Worker 保持活跃状态:

/**
 * Tracks when a service worker was last alive and extends the service worker
 * lifetime by writing the current time to extension storage every 20 seconds.
 * You should still prepare for unexpected termination - for example, if the
 * extension process crashes or your extension is manually stopped at
 * chrome://serviceworker-internals. 
 */
let heartbeatInterval;

async function runHeartbeat() {
  await browser.storage.local.set({ 'last-heartbeat': new Date().getTime() });
}

/**
 * Starts the heartbeat interval which keeps the service worker alive. Call
 * this sparingly when you are doing work which requires persistence, and call
 * stopHeartbeat once that work is complete.
 */
async function startHeartbeat() {
  // Run the heartbeat once at service worker startup.
  runHeartbeat().then(() => {
    // Then again every 20 seconds.
    heartbeatInterval = setInterval(runHeartbeat, 20 * 1000);
  });
}

async function stopHeartbeat() {
  clearInterval(heartbeatInterval);
}

/**
 * Returns the last heartbeat stored in extension storage, or undefined if
 * the heartbeat has never run before.
 */
async function getLastHeartbeat() {
  return (await browser.storage.local.get('last-heartbeat'))['last-heartbeat'];
}