Skip to content

背景脚本 (Background Scripts)

浏览器扩展开发者经常需要在浏览器的后台执行JavaScript。背景脚本,顾名思义,就是解决这个问题的答案:它们是一个独立的JavaScript运行时,可以与扩展的所有不同部分进行通信,为许多不同的浏览器事件添加处理程序,并且可以在不依赖任何网页或扩展用户界面的情况下运行。

注意:本章将重点介绍manifest v3中作为service workers实现的背景脚本。Manifest v2中的背景页面(background pages)功能更为强大。然而,Google Chrome正在逐步淘汰对它们的支持,因此我不建议构建明确依赖manifest v2背景页面的浏览器扩展。

网页Service Workers与扩展Service Workers

对于刚开始接触浏览器扩展的Web开发者来说,背景service workers的性质是一个常见的困惑点。尽管许多Web开发者在构建网页时已经接触过service workers,但扩展中的service workers在性质上有一些显著的不同。

下面我们来对比一个简单的网页service worker和一个简单的浏览器扩展service worker的例子:

网页service worker

javascript
// 定义一个缓存名称
const cacheName = 'cache_v1';
// 需要预缓存的资源
const precachedAssets = [
  '/img1.jpg',
  '/img2.jpg',
  '/img3.jpg'
];

self.addEventListener('install', (event) => {
  // 在安装时预缓存资源
  event.waitUntil(caches.open(cacheName).then((cache) => {
    return cache.addAll(precachedAssets);
  }));
});

self.addEventListener('fetch', (event) => {
  // 测试请求是否与预缓存的资源匹配
  const url = new URL(event.request.url);
  const isPrecachedRequest = precachedAssets.includes(url.pathname);
  
  if (isPrecachedRequest) {
    // 从缓存中获取预缓存的资源
    event.respondWith(caches.open(cacheName).then((cache) => {
      return cache.match(event.request.url);
    }));
  } else {
    // 访问网络
    return;
  }
});

这个简单的网页service worker为一些图片定义了一个缓存。在安装事件发生时,它会将这些图片预加载到缓存中。当它看到一个与这些图片URL匹配的fetch事件时,它会拦截请求并从缓存中返回值。

扩展service worker

javascript
// 这将在安装或更新时运行
chrome.runtime.onInstalled.addListener(() => {
  console.log('Installed or updated!');
});

// 这将在创建书签时运行
chrome.bookmarks.onCreated.addListener(() => {
  console.log('Created a new bookmark!');
});

const filter = {
  url: [
    {
      urlMatches: 'https://www.example.com/',
    },
  ],
};

// 这将在访问示例网站时运行
chrome.webNavigation.onCompleted.addListener(() => {
  console.log("Loaded the example site!");
}, filter);

这个简单的扩展service worker为runtime.onInstalled、bookmark.onCreated和webNavigation.onCompleted事件设置了监听器。当这些事件被触发时,每个处理器都会在控制台中打印一条日志语句。

相似之处

W3C的service worker规范从一开始就给出了以下高级特征描述:该规范的核心是一个接收事件而被唤醒的worker。在这一方面,网页service worker和扩展service worker的目的是相同的。此外,“网页”service worker和“扩展”service worker之间的区别仅在于它们如何在各自的上下文中被部署;而底层的service worker平台则保持不变。

在比较上面两个例子时,你可能会注意到以下几点相似之处:

  • 两个service worker脚本都主要由在脚本顶层分配的事件处理程序组成。这意味着它们都以事件驱动的方式工作,监听特定的事件并做出响应。
  • 两个service worker都在监听某种形式的安装事件。尽管它们监听的具体事件可能不同(例如,网页service worker监听install事件以预缓存资源,而扩展service worker监听chrome.runtime.onInstalled事件以执行安装后的操作),但它们都关注于安装过程中的某些方面。
  • 两个service worker都可以访问浏览器中发生的网络请求。虽然它们处理这些请求的方式可能不同(例如,一个可能将请求重定向到缓存,而另一个可能只是记录请求),但它们都有能力访问和(在某种程度上)影响这些请求。

这些相似之处强调了这两种service worker之间的许多共同点。考虑到这些代码上的相似性,让我们进一步探讨这些service worker上下文在哪些方面是相似的。

单用户占用

对于网页服务工作者和扩展服务工作者而言,每个脚本都只会有一个服务工作者。在安装服务工作者的更新时,浏览器会小心地将旧的服务工作者替换为新的,同时确保在任何给定时间都只有一个服务工作者处于活动状态。

这在两种情况下都至关重要:如果有多个服务工作者,缓存、网络拦截和事件处理将会陷入一片混乱。

安装与生命周期

所有服务工作者都只会被安装一次,之后可能会进入空闲状态并最终被终止。浏览器会决定何时唤醒服务工作者,这通常是因为触发了某个事件,而服务工作者的脚本每次都会被重新执行。

尽管“已安装”事件的概念对于网页和扩展有不同的含义,但这些处理程序的使用方式保持不变。开发者应该预期服务工作者的脚本会被无限次地执行,但已安装的处理程序只会执行一次,因此应该利用它来执行不应重复的设置工作。

顶层事件处理

由于服务工作者会经历一个重复的周期,包括变为空闲、被终止和重新激活,因此服务工作者的脚本结构必须考虑到这一点。浏览器触发并传递给服务工作者的事件,必须在事件循环的第一个回合中添加处理程序——否则可能会错过该事件。

原因是服务工作者通常是为了直接响应某个事件的触发而被唤醒的。浏览器会唤醒服务工作者,执行一个事件循环回合,然后在服务工作者中触发排队的事件。如果在该回合结束时该事件的处理程序不存在,则事件可能会未经处理而通过。

异步消息传递

由于所有服务工作者都是严格异步的,因此它们必须使用某种形式的异步消息传递来与浏览器的其他部分进行通信。

  • 对于网页服务工作者,这采用postMessage()MessageChannel API的形式。
  • 对于浏览器扩展,这采用runtime.sendMessage()tabs.sendMessage()runtime.connect()tabs.connect()的形式。

差异

尽管网页服务工作者和扩展服务工作者使用相同的底层平台,但它们在几个重要方面存在差异。

注册

网页服务工作者必须从页面级脚本进行注册。以下是一个示例:

示例网页服务工作者注册

javascript
if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js')
    .then((registration) => {
      registration.addEventListener('updatefound', () => {
        console.log('A new service worker is being installed:');
      });
    })
    .catch((error) => {
      console.error(`Service worker registration failed: ${error}`);
    });
} else {
  console.error('Service workers are not supported.');
}

对于扩展服务工作者,注册服务工作者的唯一步骤是在清单中指定background.service_worker脚本。网页服务工作者的注册包括安装/等待/活动状态,这些状态对于扩展服务工作者来说没有意义。

注意:有关网页服务工作者的更多信息,请参阅https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API。

目的

网页服务工作者和扩展服务工作者之间最重要的区别是它们的主要用途。网页服务工作者能够充当缓存,这是它们最常见的用例。它们能够有条件地拦截网络请求并返回缓存的内容。服务工作者还可用于构建渐进式Web应用(PWA),即具有安装、离线功能和服务器发送推送通知等应用行为的网页。

由于浏览器扩展脚本是资产且全部由浏览器提供,因此无需再缓存这些资源,因为它们不是从远程服务器加载的。此外,弹出页面和选项页面等接口即使不使用后台服务工作者也能在离线状态下正常运行。相反,扩展服务工作者的主要任务是处理浏览器和WebExtensions API触发的一系列广泛事件。

注意:在本章的其余部分中,扩展服务工作者将简称为“服务工作者”。

Manifest V2 vs. Manifest V3

Manifest V3 对背景脚本进行了重大变革。在 Manifest V2 中,背景脚本被视为背景页面:在无头网页中执行的 JavaScript。这些背景页面还能无限期运行,意味着它们适合管理长时间运行的操作和网络请求。而在 Manifest V3 中,背景脚本现在作为服务工作线程执行。这些工作线程更轻量级,但在一些重要方面有所缩减。

脚本 vs. 服务工作线程

在 Manifest V2 中,manifest 的 background 值可以传递一个脚本数组:

Manifest V2 manifest.json

json
{
  "manifest_version": 2,
  ...
  "background": {
    "scripts": ["bg1.js", "bg2.js", "bg3.js"]
  }
  ...
}

这些脚本按照它们在数组中出现的顺序加载,并都在同一个背景页面中执行。而在 Manifest V3 中,背景脚本是一个单一的服务工作线程:

Manifest V3 manifest.json

json
{
  "manifest_version": 3,
  ...
  "background": {
    "service_worker": "bg.js"
  }
  ...
}

JavaScript 导入

在 Manifest V2 中,使用 JavaScript 导入的唯一方法是通过设置 background.page 属性,并在背景页面 HTML 中使用 script 标签:

manifest.json 启用背景 HTML 页面

json
{
  "manifest_version": 2,
  ...
  "background": {
    "page": "bg.html"
  }
  ...
}

bg.html

html
<html>
<body>
    <!-- 此 JS 文件中允许的导入 -->
    <script type="module" src="bg.js"></script>
</body>
</html>

在 Manifest V3 中,只需将 background.module 属性设置为 "module" 即可允许使用 import 关键字:

Manifest V3 manifest.json 允许使用 import 关键字

json
{
  "manifest_version": 3,
  ...
  "background": {
    "service_worker": "bg.js",
    "type": "module"
  }
  ...
}

无法访问DOM和全局API受限

在Manifest v2中,背景页面实际上是无头网页,可以完全访问DOM和网页全局对象。然而,在Manifest v3中,服务工作线程无法访问DOM,其全局对象是ServiceWorkerGlobalScope,缺少网页全局对象中的许多API。这带来了一些重要影响:

  • 无法访问文档对象。DOM及其所有相关方法都不再可用。这也意味着背景服务工作线程无法再创建和缓存资源。

  • 无法在其他上下文中渲染内容以供显示。在某些情况下,在无头页面中渲染内容很有用,但现在已不可能实现。部分解决方案包括使用第三方库(如jsdom)或使用OffscreenCanvas API。

  • 无法访问window对象。这意味着像window.open()这样的方法不再可用。

  • 无法在服务工作线程中直接播放或捕获媒体。虽然仍然可以实现,但需要主机活动内容脚本或扩展视图来授予对媒体API的访问权限。

  • 无法访问localStoragesessionStoragecookies

  • 无法使用XMLHttpRequest。应使用fetch()

注意:失去对window对象的访问权,在尝试从服务工作线程进行身份验证时尤其成问题,因为第三方身份验证库通常依赖于打开一个新窗口来进行OAuth流程。有关身份验证的更多信息,请参阅“网络”章节。

非持久性

Manifest v2允许背景页面为“持久性”的,这实际上意味着只要宿主浏览器程序打开,它们就会无限期地运行。然而,在Manifest v3中,这一功能被移除了。这带来了以下几个影响:

  • setTimeout()setInterval()将不再可靠执行。这些方法仍然可用,但是,如果服务工作线程在计划执行的函数之前终止,那么该函数将静默地被跳过。chrome.alarms API可以替代部分功能,但它不能用于设置小于一分钟的时间间隔。

  • 长期运行的查询和WebSocket连接可能会被终止。服务工作线程在确定其是否空闲时,会忽略打开的网络连接,这意味着如果这些连接在服务工作线程终止时仍然处于活动状态,那么这些连接可能会被断开。

提示:在本章的后续部分,有一些技巧可以延长服务工作线程的生命周期。

无关闭事件

Manifest v2允许开发者为runtime.onSuspendruntime.onSuspendCanceled事件添加处理程序。这为扩展提供了进行清理工作的机会。然而,在Manifest v3中,服务工作线程没有关闭事件的处理程序,这意味着服务工作线程脚本必须积极地执行任务,并预期随时可能发生关闭。

无程序化后台访问

在Manifest v2中,可以使用chrome.extension.getBackgroundPage()从前台的扩展接口访问后台窗口对象。但在Manifest v3中,这一功能已被移除。

与后台脚本一起工作

让我们从一个带有后台脚本的简单浏览器扩展开始。这通过在manifest中定义background.service_worker属性来实现:

示例7-1a. manifest.json

json
{
  "name": "MVX",
  "version": "0.0.1",
  "manifest_version": 3,
  "background": {
    "service_worker": "background.js"
  }
}

示例7-1b. background.js

javascript
console.log("后台脚本已初始化!");
chrome.runtime.onInstalled.addListener((object) => {
  console.log("后台脚本已安装!");
});

这个简单的后台脚本会在控制台中记录日志,设置一个安装处理程序,然后退出。