背景脚本

背景脚本或页面使您能够监控和响应浏览器中的事件,例如导航到新页面、删除书签或关闭标签页。

背景脚本或页面是

  • 持久性 - 扩展启动时加载,扩展禁用或卸载时卸载。
  • 非持久性(也称为事件页面) - 仅在需要响应事件时加载,并在处于空闲状态时卸载。但是,背景页面在所有可见视图和消息端口关闭之前不会卸载。打开视图不会导致背景页面加载,但会阻止它关闭。

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

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

在清单 V2 中,背景脚本或页面可以是持久性或非持久性。建议使用非持久性背景脚本,因为它们可以降低扩展的资源成本。在清单 V3 中,仅支持非持久性背景脚本或页面。

如果您在清单 V2 中有持久性背景脚本或页面,并且想要为迁移到清单 V3 做好准备,转换为非持久性 提供了有关将脚本或页面转换为非持久性模型的建议。

背景脚本环境

DOM API

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

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

WebExtension API

背景脚本可以使用任何 WebExtension API,只要其扩展具有必要的 权限

跨域访问

背景脚本可以向它们具有 主机权限 的主机发出 XHR 请求。

网页内容

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

内容安全策略

背景脚本被限制进行某些潜在的危险操作,例如使用 eval(),这通过内容安全策略来实现。

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

实现背景脚本

本节介绍如何实现非持久性背景脚本。

指定背景脚本

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

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

这些脚本在扩展的背景页面中执行,因此它们在相同的上下文中运行,就像加载到网页中的脚本一样。

但是,如果您需要背景页面中的某些内容,可以指定一个。然后,您从页面中指定脚本,而不是使用 "scripts" 属性。在 "background" 键中引入 "type" 属性之前,这是包含 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.onMessage removeListener。如果删除了某个事件的所有监听器,浏览器将不再为此事件加载扩展的背景脚本。

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

筛选事件

使用支持事件筛选器的 API 来限制监听器仅限于扩展关注的案例。如果扩展正在监听 tabs.onUpdated,请使用带有筛选器的 webNavigation.onCompleted 事件,因为标签页 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 错误 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 版本 3 扩展不能使用后台或事件页面。

如果你的扩展需要对后台页面 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();
});