构建跨浏览器扩展程序

浏览器扩展 API 的引入为浏览器扩展的开发创建了一个统一的平台。然而,使用扩展 API 的浏览器(主要是 Chrome、Edge、Firefox、Opera 和 Safari)在 API 实现和覆盖范围上存在差异。

要最大限度地扩大浏览器扩展的覆盖范围,意味着至少要为两种浏览器开发,甚至可能更多。本文着眼于创建跨浏览器扩展时面临的主要挑战,并提出了应对这些挑战的方法。

注意:主要浏览器已采用 Manifest V3。此 Manifest 版本在浏览器扩展环境(例如用于处理异步事件的 Promise)之间提供了更好的兼容性。除了本指南中的信息外,请参阅 FirefoxChrome 的 Manifest V3 迁移指南。

跨平台扩展编码障碍

在处理跨平台扩展时,您需要解决以下几个方面的问题:

API 命名空间

主要浏览器中使用了两种 API 命名空间:

  • browser.*,Firefox 和 Safari 使用的扩展 API 的拟议标准。
  • chrome.*,Chrome、Opera 和 Edge 使用。

Firefox 还支持 chrome.* 命名空间,用于与 Chrome 兼容的 API,主要用于协助移植。然而,首选使用 browser.* 命名空间。除了是拟议标准之外,browser.* 还使用 Promise——一种现代且方便的异步事件处理机制。

只有在最微不足道的扩展中,命名空间才可能是唯一需要解决的跨平台问题。因此,单独解决这个问题很少有用。最好的方法是结合异步事件处理来解决这个问题。

API 异步事件处理

随着 Manifest V3 的引入,所有主要浏览器都采用了从异步方法返回 *Promise* 的标准。Firefox 和 Safari 对所有异步 API 都完全支持 Promise。从 Chrome 121 开始,除非另有说明,所有异步扩展 API 都支持 Promise。`devtools` API 是唯一没有 Promise 支持的 API 命名空间 (Chromium bug 1510416)。

在 Manifest V2 中,Firefox 和 Safari 支持异步方法的 Promise。同时,Chrome 方法调用 *回调*。为了兼容性,所有主要浏览器在所有 Manifest 版本中都支持回调。详情请参见 回调和 Promise

某些扩展 API 事件的处理程序需要通过 Promise 或回调函数进行异步响应。例如,runtime.onMessage 事件的处理程序可以使用 Promise 发送异步响应或使用回调。事件处理程序返回的 Promise 在 Firefox 和 Safari 中受支持,但在 Chrome 中尚未受支持。

Firefox 还支持对支持 chrome.* 命名空间的 API 进行回调。然而,建议使用 Promise。Promise 极大地简化了异步事件处理,尤其是在您需要将事件链式连接在一起时。这意味着使用 polyfill 或类似工具,以便您的扩展在 Firefox 和 Safari 中使用 browser.* 命名空间,在 Chrome、Opera 和 Edge 中使用 chrome.*

注意:如果您不熟悉这两种方法的区别,请参阅 了解异步 JavaScript:回调、Promise 和 Async/Await 或 MDN 使用 Promise 页面。

WebExtension 浏览器 API Polyfill

那么,如何轻松利用 Promise 呢?解决方案是使用 Promise 为 Firefox 编写代码,并使用 WebExtension 浏览器 API Polyfill 来支持 Chrome、Opera 和 Edge。

此 polyfill 解决了 Firefox、Chrome、Opera 和 Edge 之间的 API 命名空间和异步事件处理问题。

要使用 polyfill,请使用 npm 将其安装到您的开发环境中,或直接从 GitHub releases 下载。

然后,在以下位置引用 browser-polyfill.js

  • manifest.json,使其可用于后台脚本和内容脚本。
  • HTML 文档,例如 browserAction 弹出窗口或选项卡页面。
  • tabs.executeScript 动态注入的内容脚本中的 executeScript 调用,如果它尚未通过 manifest.json 中的 content_scripts 声明加载。

例如,此 manifest.json 代码使 polyfill 可用于后台脚本。

json
{
  // …
  "background": {
    "scripts": ["browser-polyfill.js", "background.js"]
  }
}

您的目标是确保 polyfill 在您的扩展中执行,然后执行任何其他需要 browser.* API 命名空间的脚本。

注意:有关使用 polyfill 与模块打包器的更多详细信息和信息,请参阅项目在 GitHub 上的自述文件。

还有其他 polyfill 选项。然而,在撰写本文时,其他选项都没有提供 WebExtension 浏览器 API Polyfill 的覆盖范围。因此,如果您没有将 Firefox 作为首选目标,您的选择是接受替代 polyfill 的限制,移植到 Firefox 并添加跨浏览器支持,或者开发自己的 polyfill。

API 函数覆盖

主要浏览器中提供的 API 函数差异分为三大类:

  1. 缺乏对整个函数的支持。例如,在撰写本文时,Edge 不支持 browserSettings 函数。
  2. 函数内功能支持的差异。例如,在撰写本文时,Firefox 不支持通知函数方法 notifications.onButtonClicked,而 Firefox 是唯一支持 notifications.onShown 的浏览器。
  3. 专有功能,支持特定于浏览器的功能。例如,在撰写本文时,容器是 Firefox 特有的功能,由 contextualIdentities 函数支持。

有关主要浏览器、Firefox for Android 和 Safari on iOS 对扩展 API 支持的详细信息,可以在 Mozilla Developer Network 的JavaScript API 浏览器支持页面上找到。浏览器兼容性信息也包含在 Mozilla Developer Network JavaScript API 参考页面的每个函数及其方法、类型和事件中。

处理 API 差异

解决 API 差异的一种简单方法是将您的扩展中使用的函数限制为在所有目标浏览器中提供相同功能的函数。实际上,这种方法对于大多数扩展来说可能过于严格。

相反,当 API 之间存在差异时,您应该提供替代实现或回退功能。(请记住:您可能还需要这样做,以考虑*相同*浏览器不同版本之间 API 支持的差异。)

使用运行时检查函数功能的可用性是实现替代或回退功能的推荐方法。执行运行时检查的好处是,当函数可用时,您无需更新和重新分发扩展即可利用该函数。

以下代码使您能够执行运行时检查:

js
if (typeof fn === "function") {
  // safe to use the function
}

内容脚本执行上下文

内容脚本可以访问和修改页面的 DOM,就像页面脚本一样。它们还可以查看页面脚本对 DOM 所做的任何更改。然而,内容脚本获得的是 DOM 的“干净”视图。

Firefox 和 Chrome 使用根本不同的方法来处理此行为:在 Firefox 中,它被称为 Xray 视觉,而 Chrome 使用隔离世界。有关更多详细信息,请参阅内容脚本概念文章的内容脚本环境部分。

然而,Firefox 提供了一些 API,使内容脚本能够访问由页面脚本创建的 JavaScript 对象,并将其 JavaScript 对象暴露给页面脚本。有关详细信息,请参阅与页面脚本共享对象

内容脚本的内容安全策略 (CSP) 也存在差异。

后台页面和扩展服务工作线程

作为 Manifest V3 实现的一部分,Chrome 用扩展服务工作线程取代了后台页面。Firefox 保留了后台页面的使用,而 Safari 支持后台页面和服务工作线程。

有关更多信息,请参阅 "background" Manifest 键页面上的浏览器支持部分。其中包括如何实现跨浏览器脚本的示例。

Manifest 键

主要浏览器支持的 manifest.json 文件键的差异大致分为三类:

  1. 扩展信息属性。例如,在撰写本文时,Firefox 和 Opera 包含 developer 键,用于提供扩展开发者的详细信息。
  2. 扩展功能。例如,在撰写本文时,Chrome 不支持 browser_specific_settings 键。
  3. 键的可选性。在撰写本文时,通常只有 "manifest_version""version""name" 是强制键。

浏览器兼容性信息包含在 Mozilla Developer Network manifest.json 键参考页面的每个键中。

由于 manifest.json 文件很少更改——除了发布版本号,这可能在不同浏览器之间有所不同——为每个浏览器创建和编辑一个静态版本通常是最简单的方法。

扩展打包

为通过浏览器扩展商店分发而打包扩展程序相对简单。Firefox、Chrome、Edge 和 Opera 都使用简单的 zip 格式,要求 manifest.json 文件位于 zip 包的根目录。Safari 要求扩展程序以类似于应用程序的方式打包。

有关打包的详细信息,请参阅各个扩展程序开发者门户上的指南。

扩展发布

各大浏览器都维护着浏览器扩展商店。每个商店还会审查您的扩展程序,以检查是否存在安全漏洞。

因此,您需要单独处理每个商店的扩展程序添加和更新。

下表总结了每个商店的方法和功能

浏览器 注册费 上传工具 发布前审核流程 账户双重身份验证

Chrome

自动,不到一小时

Edge

未提供 SLA

Firefox

web-ext

自动,几秒钟。

扩展发布后会进行手动审查,如果发现需要修复的问题,可能会导致扩展暂停。

Opera

手动,未提供 SLA

Safari

有,根据 Apple 的说法,平均 50% 的应用程序在 24 小时内完成审查,超过 90% 的应用程序在 48 小时内完成审查。

其他注意事项

版本号

Firefox、Chrome 和 Edge 商店要求每个上传的版本都有不同的版本号。这意味着如果您在发布版本中遇到问题,您不能回滚到早期版本号。

总结

在进行跨平台扩展开发时,可以通过将目标设置为 Firefox 并使用 WebExtension 浏览器 API Polyfill 来解决扩展 API 实现之间的差异。

您的大部分跨平台工作可能会集中在处理主要浏览器支持的 API 功能之间的差异。您可能还需要考虑内容脚本和后台脚本实现之间的差异。创建 manifest.json 文件应该相对简单,并且您可以手动完成。然后,您需要考虑提交到每个扩展商店的过程中的差异。

遵循本文中的建议,您应该能够创建一个在所有四个主要浏览器上都能良好运行的扩展,从而使您的扩展功能能够惠及更多人。