后台脚本

后台脚本或后台页面允许你监控并响应浏览器中的事件,例如导航到新页面、删除书签或关闭标签页。

后台脚本或页面分为:

  • 持久型 - 扩展启动时加载,扩展禁用或卸载时卸载。
  • 非持久型(也称为事件页面) - 仅在需要响应事件时加载,空闲时卸载。然而,后台页面在所有可见视图和消息端口关闭之前不会卸载。打开视图不会导致后台页面加载,但会阻止其关闭。

注意:在 Firefox 中,如果扩展进程崩溃

  • 崩溃时运行的持久型后台脚本会自动重新加载。
  • 崩溃时运行的非持久型后台脚本(也称为“事件页面”)不会重新加载。但是,当 Firefox 调用其 WebExtensions API 事件监听器之一时,它们会自动重新启动。
  • 崩溃时加载在标签页中的扩展页面不会自动恢复。每个标签页中的警告消息会通知用户页面已崩溃,并允许用户关闭或恢复标签页。 显示用户消息的浏览器窗口,指示页面已崩溃,并提供关闭或重新启动标签页的选项 你可以通过打开新标签页并导航到 about:crashextensions 来测试此情况,这将静默触发扩展进程崩溃。

在 Manifest V2 中,后台脚本或页面可以是持久型或非持久型。建议使用非持久型后台脚本,因为它们可以减少扩展的资源消耗。在 Manifest V3 中,只支持非持久型后台脚本或页面。

如果你在 Manifest V2 中有持久型后台脚本或页面,并希望为扩展迁移到 Manifest V3 做准备,请参阅转换为非持久型,其中提供了将脚本或页面转换为非持久型模型的建议。

后台脚本环境

DOM API

后台脚本在名为后台页面的特殊页面的上下文中运行。这为它们提供了 window 全局对象,以及该对象提供的所有标准 DOM API。

警告:在 Firefox 中,后台页面不支持使用 alert()confirm()prompt()

WebExtension API

只要扩展具有必要的权限,后台脚本就可以使用任何WebExtension API

跨域访问

后台脚本可以向其拥有主机权限的主机发出 XHR 请求。

Web 内容

后台脚本无法直接访问网页。但是,它们可以将内容脚本加载到网页中,并使用消息传递 API 与这些内容脚本通信

内容安全策略

通过内容安全策略,后台脚本被限制执行某些潜在危险的操作,例如使用 eval()

有关详细信息,请参阅内容安全策略

实现后台脚本

本节描述了如何实现非持久型后台脚本。

指定后台脚本

在你的扩展中,如果需要,可以使用 manifest.json 中的 "background" 键包含一个或多个后台脚本。对于 Manifest V2 扩展,persistent 属性必须为 false 才能创建非持久型脚本。对于 Manifest V3 扩展,可以省略该属性或必须将其设置为 false,因为 Manifest V3 中的脚本始终是非持久型的。包含 "type": "module" 会将后台脚本作为 ES 模块加载。

json
"background": {
  "scripts": ["background-script.js"],
  "persistent": false,
  "type": "module"
}

这些脚本在扩展的后台页面中执行,因此它们在与加载到网页中的脚本相同的上下文中运行。

但是,如果后台页面中需要特定内容,你可以指定一个。然后,你可以从页面而不是使用 "scripts" 属性指定脚本。在将 "type" 属性引入 "background" 键之前,这是包含 ES 模块的唯一选项。你可以这样指定后台页面

  • manifest.json

    json
    "background": {
      "page": "background-page.html",
      "persistent": false
    }
    
  • background-page.html

    html
    <!doctype html>
    <html lang="en">
      <head>
        <meta charset="utf-8" />
        <script type="module" src="background-script.js"></script>
      </head>
    </html>
    

你不能同时指定后台脚本和后台页面。

初始化扩展

监听 runtime.onInstalled 以在安装时初始化扩展。使用此事件来设置状态或进行一次性初始化。

对于带有事件页面的扩展,应该在此处使用有状态 API,例如使用 menus.create 创建的上下文菜单。这是因为有状态 API 不需要每次事件页面重新加载时都运行;它们只需要在安装扩展时运行。

js
browser.runtime.onInstalled.addListener(() => {
  browser.contextMenus.create({
    id: "sampleContextMenu",
    title: "Sample Context Menu",
    contexts: ["selection"],
  });
});

添加监听器

围绕扩展依赖的事件来构建后台脚本。定义相关事件使后台脚本能够保持休眠状态,直到这些事件触发,并防止扩展错过重要的触发器。

监听器必须从页面开始处同步注册。

js
browser.runtime.onInstalled.addListener(() => {
  browser.contextMenus.create({
    id: "sampleContextMenu",
    title: "Sample Context Menu",
    contexts: ["selection"],
  });
});

// This will run when a bookmark is created.
browser.bookmarks.onCreated.addListener(() => {
  // do something
});

不要异步注册监听器,因为它们将无法正确触发。因此,不要这样做

js
window.onload = () => {
  // WARNING! This event is not persisted, and will not restart the event page.
  browser.bookmarks.onCreated.addListener(() => {
    // do something
  });
};

而是这样做

js
browser.tabs.onUpdated.addListener(() => {
  // This event is run in the top level scope of the event page, and will persist, allowing
  // it to restart the event page if necessary.
});

扩展可以通过调用 removeListener(例如,与 runtime.onMessageremoveListener 一起使用)从其后台脚本中删除监听器。如果某个事件的所有监听器都被删除,浏览器将不再为该事件加载扩展的后台脚本。

js
browser.runtime.onMessage.addListener(
  function messageListener(message, sender, sendResponse) {
    browser.runtime.onMessage.removeListener(messageListener);
  },
);

过滤事件

使用支持事件过滤的 API 将监听器限制在扩展关心的特定情况。如果一个扩展正在监听 tabs.onUpdated,请改为使用带过滤器的 webNavigation.onCompleted 事件,因为 tabs API 不支持过滤器。

js
browser.webNavigation.onCompleted.addListener(
  () => {
    console.log("This is my favorite website!");
  },
  { url: [{ urlMatches: "https://www.mozilla.org/" }] },
);

响应监听器

监听器的作用是在事件触发后触发功能。要响应事件,请在监听器事件内部构建所需的响应。

当在特定标签页或框架的上下文中响应事件时,请使用事件详细信息中的 tabIdframeId,而不是依赖于“当前标签页”。指定目标可确保当“当前标签页”在唤醒事件页面时发生更改时,你的扩展不会在错误的目标上调用扩展 API。

例如,runtime.onMessage 可以响应 runtime.sendMessage 调用,如下所示

js
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.data === "setAlarm") {
    browser.alarms.create({ delayInMinutes: 5 });
  } else if (message.data === "runLogic") {
    browser.scripting.executeScript({
      target: {
        tabId: sender.tab.id,
        frameIds: [sender.frameId],
      },
      files: ["logic.js"],
    });
  } else if (message.data === "changeColor") {
    browser.scripting.executeScript({
      target: {
        tabId: sender.tab.id,
        frameIds: [sender.frameId],
      },
      func: () => {
        document.body.style.backgroundColor = "orange";
      },
    });
  }
});

卸载后台脚本

数据应定期持久化,以避免在扩展崩溃而未收到 runtime.onSuspend 时丢失重要信息。使用存储 API 来辅助此操作。

js
// Or storage.session if the variable does not need to persist pass browser shutdown.
browser.storage.local.set({ variable: variableInformation });

消息端口不能阻止事件页面关闭。如果扩展使用消息传递,当事件页面空闲时,端口将关闭。监听 runtime.PortonDisconnect 允许你发现何时关闭打开的端口,但是该监听器受到与 runtime.onSuspend 相同的时间限制。

js
browser.runtime.onConnect.addListener((port) => {
  port.onMessage.addListener((message) => {
    if (message === "hello") {
      let response = { greeting: "welcome!" };
      port.postMessage(response);
    } else if (message === "goodbye") {
      console.log("Disconnecting port from this end");
      port.disconnect();
    }
  });
  port.onDisconnect.addListener(() => {
    console.log("Port was disconnected from the other end");
  });
});

后台脚本在几秒钟不活动后卸载。但是,如果在后台脚本暂停期间另一个事件唤醒了后台脚本,则会调用 runtime.onSuspendCanceled,并且后台脚本会继续运行。如果需要任何清理,请监听 runtime.onSuspend

js
browser.runtime.onSuspend.addListener(() => {
  console.log("Unloading.");
  browser.browserAction.setBadgeText({ text: "" });
});

然而,应该优先选择持久化数据,而不是依赖 runtime.onSuspend。它不允许进行所需的那么多清理,并且在崩溃的情况下也无济于事。

转换为非持久型

如果你有一个持久型后台脚本,本节提供了将其转换为非持久型模型的说明。

更新 manifest.json 文件

在扩展的 manifest.json 文件中,将 "background" 键的 persistent 属性更改为 false,适用于你的脚本或页面。

json
"background": {
  …,
  "persistent": false
}

移动事件监听器

监听器必须位于顶级,以便在事件触发时激活后台脚本。注册的监听器可能需要重新组织为同步模式并移动到顶级。

js
browser.runtime.onStartup.addListener(() => {
  // run startup function
});

记录状态更改

脚本现在根据需要打开和关闭。因此,不要依赖全局变量。

js
var count = 101;
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message === "count") {
    ++count;
    sendResponse(count);
  }
});

而是使用存储 API 来设置和返回状态和值

  • 使用 storage.session 进行内存存储,该存储在扩展或浏览器关闭时清除。默认情况下,storage.session 仅适用于扩展上下文,不适用于内容脚本。
  • 使用 storage.local 获取更大的存储区域,该区域在浏览器和扩展重新启动后仍然存在。
js
browser.runtime.onMessage.addListener(async (message, sender) => {
  if (message === "count") {
    let items = await browser.storage.session.get({ myStoredCount: 101 });
    let count = items.myStoredCount;
    ++count;
    await browser.storage.session.set({ myStoredCount: count });
    return count;
  }
});

前面的示例使用 Promise 发送异步响应,这在 Chrome 中直到Chrome bug 1185241 解决才支持。一个跨浏览器替代方案是返回 true 并使用 sendResponse

js
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message === "count") {
    browser.storage.session.get({ myStoredCount: 101 }).then(async (items) => {
      let count = items.myStoredCount;
      ++count;
      await browser.storage.session.set({ myStoredCount: count });
      sendResponse(count);
    });
    return true;
  }
});

将定时器更改为警报

基于 DOM 的定时器,例如 setTimeout(),在事件页面空闲后不再保持活动状态。相反,如果你需要定时器来唤醒事件页面,请使用 alarms API。

js
browser.alarms.create({ delayInMinutes: 3.0 });

然后添加一个监听器。

js
browser.alarms.onAlarm.addListener(() => {
  alert("Hello, world!");
});

更新后台脚本函数调用

扩展通常将其主要功能托管在后台脚本中。一些扩展通过 extension.getBackgroundPage 返回的 window 访问后台页面中定义的函数和变量。该方法在以下情况下返回 null

  • 扩展页面是隔离的,例如隐私浏览模式或容器标签页中的扩展页面。
  • 后台页面未运行。这在持久型后台页面中不常见,但在使用事件页面时很可能发生,因为事件页面可能会被暂停。

注意:建议通过 runtime.sendMessage()runtime.connect() 与后台脚本通信来调用其功能。本节讨论的 getBackgroundPage() 方法不能用于跨浏览器扩展,因为 Chrome 中的 Manifest V3 扩展不能使用后台或事件页面。

如果你的扩展需要后台页面的 window 引用,请使用 runtime.getBackgroundPage 以确保事件页面正在运行。如果调用是可选的(即,仅在事件页面处于活动状态时才需要),则使用 extension.getBackgroundPage

js
document.getElementById("target").addEventListener("click", async () => {
  let backgroundPage = browser.extension.getBackgroundPage();
  // Warning: backgroundPage is likely null.
  backgroundPage.backgroundFunction();
});
js
document.getElementById("target").addEventListener("click", async () => {
  // runtime.getBackgroundPage() wakes up the event page if it was not running.
  let backgroundPage = await browser.runtime.getBackgroundPage();
  backgroundPage.backgroundFunction();
});