Skip to content

跨浏览器扩展

在支持多个平台方面,浏览器扩展介于网站和移动应用之间。谱系的一端是网站,它能让开发者享受“一次编写,随处运行”的体验。谱系的另一端是移动应用,它采用专有语言编写,仅能在特定操作系统和部分设备类型上运行。在本章中,我们将讨论在多个浏览器上支持扩展所涉及的挑战和权衡。

跨浏览器支持简介

网站代码享有一致的主机环境。你可以编写一次网站代码,然后将其加载到任何操作系统、设备或浏览器上,并期望它能正常运行。虽然存在一些浏览器差异的特殊情况,但这些情况相对较少。所有浏览器厂商都花费大量时间确保其产品符合 W3C 标准,并且会以与其他浏览器大致相同的方式渲染网页。

移动应用运行于专属的主机环境中。若想让一款移动应用在多个操作系统上运行,那么为安卓系统编写的代码在构建 iOS 应用时几乎毫无用处。应用分发于不同的平台,采用不同的编程语言编写,为不同的设备打造,并且使用完全不同的软件开发工具包(SDK)。

浏览器扩展从上述每种模式中都借鉴了一些元素。扩展使用在浏览器间标准化的应用程序编程接口(API),但并非所有浏览器都支持完整的 API。所有扩展均采用 HTML、JavaScript 和 CSS 构建,不过浏览器间的清单支持存在一些特性差异和不兼容之处。扩展在特定于浏览器的应用商店中分发,但大多数网络浏览器都是基于 Chromium 构建的,这意味着它们都能安装由 Chrome 网上应用店分发的应用。

浏览器覆盖范围的权衡

在决定是否支持以及在多大程度上支持多个浏览器时,你应该首先问问自己目标用户是谁。对于大多数扩展来说,这个问题的答案将是“尽可能多的人”!支持所有主流浏览器当然是可行的,但根据你的扩展情况,这可能有些大材小用。API 支持的差异、多个应用商店以及惯用的 JavaScript/CSS 引擎可能会耗费开发者的时间。

浏览器市场份额

幸运的是,对于刚开始起步的扩展来说,你可以非常轻松地通过单一代码库支持大多数用户。请考虑 2022 年桌面浏览器市场份额的以下数据(图 15 - 1)。 15-1

谷歌浏览器(Google Chrome)以66%的市场份额占据主导地位。如果您仅将扩展程序部署到Chrome网上应用店(Chrome Web Store),便自动覆盖了三分之二的市场。

基于Chromium内核的浏览器扩展程序共享

幸运的是,现代浏览器的发展形势对您有利。许多主流浏览器(如Edge、Opera、Vivaldi和Brave)均基于开源的Chromium浏览器构建。因此,这些浏览器与为谷歌浏览器开发的扩展程序几乎完全兼容。这些浏览器均可直接从Chrome网上应用店安装扩展程序,或通过配置实现从该平台安装。因此,仅发布到Chrome网上应用店的扩展程序实际上可被Chrome、Edge和Opera用户安装,覆盖超过80%的桌面端流量!

请注意:谷歌浏览器(Google Chrome)与其他基于Chromium内核的浏览器之间存在细微差异,例如身份验证API。不过,所有Chromium浏览器之间的API总体兼容性几乎天衣无缝。

调整代码库

根据您希望扩展程序支持的广泛程度,组织代码库可能需要一些技巧。在本节中,我们将介绍一些可用的简单原则。

API探测

并非所有浏览器都支持完整的WebExtensions API。如果某个特定方法对您的扩展程序至关重要,而浏览器不支持该方法,那么您将无能为力。在这种情况下,最好是不支持该浏览器,并鼓励用户使用受支持的浏览器。

然而,如果您希望使用的API并非至关重要,且扩展程序即使在没有该API的情况下也能运行(即使处于降级状态),那么您可以安全地探测API或方法以检查其是否可用。例如:

javascript
if (chrome.storage.session) {
    // API可用,正常继续
} else {
    // API不可用。用户体验选项:
    // - 回退到类似的API
    // - 禁用或隐藏按钮
    // - 通知用户
    // - 静默降级
}

差异化清单

在开发模式下工作时,您会发现大多数浏览器在清单结构方面相对宽松。然而,在部署到市场时,您可能会遇到两种不同市场允许或不允许的内容存在严格不兼容的情况。

为解决此问题,您需要为每个部署包生成差异化清单。一种常见模式是维护一个单一的“主”清单,然后在生成部署包时对每个市场进行特定修改。

提示:差异化清单生成可能相当繁琐。像plasmo(https://www.plasmo.com/)这样的扩展程序开发平台可以为您自动化此过程。

清单v2/v3

在本书出版时,扩展程序开发者陷入了一个尴尬的境地,即像Chrome网上应用店这样的一些市场只接受清单v3,而像Firefox附加组件市场这样的其他市场只接受清单v2。这种现状似乎很可能是暂时的,很快所有主要市场都将围绕清单v3达成一致。我不建议对清单v2和v3同时提供支持,因为两者之间的行为差异不值得弥合;您应该针对清单v3提供支持。

扩展程序市场

就像移动应用程序必须从应用商店安装一样,扩展程序也必须从扩展程序市场安装。所有主要浏览器都维护着自己的扩展程序商店。

市场相似性

以下是扩展程序市场之间一致的功能列表:

• 需要开发者帐户。没有市场允许您匿名发布浏览器扩展程序。您始终可以创建一个不透露您个人身份的独立开发者帐户。

• 所有新扩展程序和扩展程序更新都需要经过审核。每个提交的扩展程序版本在获得批准之前都会经过某种审核流程。有些审核是自动的,有些是人工的。您在扩展程序清单中请求的权限可能会改变审核的严格程度。

• 所有扩展程序都需要一些用于列表的副本和资源。图标、标题、描述、宣传图片以及指向托管网站的链接只是您在发布扩展程序时可以提供的一些内容。有些市场会自动从清单中提取部分内容。此外,不同的市场需要不同的内容。

• 所有市场(Safari除外)都支持手动发布。这涉及上传一个包含扩展程序所有资源和文件的zip文件。商店将对该zip文件进行自动分析,以检查其是否有效,如果有效,则将其添加到审核队列中。

• 所有市场都支持自动发布。这在“扩展程序开发与部署”一章中有所介绍。

市场差异

以下是扩展程序市场之间不同的功能列表:

• 市场具有不同的自动文件检查流程。代码检查、文件类型检查、文件大小和清单验证只是每个市场在实现上略有不同的几个类别。

• 市场将以不同的速度审核提交内容。Chrome网上应用店通常需要几天时间来批准提交内容,而Mozilla附加组件市场几乎会立即批准提交内容。

Chrome网上应用店

https://chrome.google.com/webstore

Chrome网上应用店是目前最大且最受欢迎的浏览器扩展程序市场(图15-2)。它拥有180,000个谷歌浏览器扩展程序,大约是苹果应用商店180万个移动应用程序的10%。如本章前面所述,所有Chromium浏览器都能够直接从此市场安装扩展程序。要发布扩展程序,您需要一个Chrome网上应用店开发者帐户。它需要一次性支付5美元的注册费。

15-2

Firefox 附加组件

https://addons.mozilla.org/

将扩展程序发布到 Firefox 扩展程序市场是免费的(图 15-3)。 15-3

Microsoft Edge 附加组件

https://microsoftedge.microsoft.com/addons/Microsoft-EdgeExtensions-Home

将扩展程序发布到 Microsoft Edge 扩展程序市场是免费的(图 15-4)。 15-4

Safari 扩展程序应用商店

Safari 扩展程序商店只能通过 Safari 浏览器访问(图 15-5)。将扩展程序发布到应用商店需要 Apple 开发者账户,费用为 99 美元/年。此外,还需要安装 Xcode 来构建扩展程序并将其部署到应用商店。 15-5

Opera 扩展程序商店

https://addons.opera.com/

将扩展程序发布到 Opera 扩展程序市场是免费的(图 15-6)。 15-6

移动端扩展程序

移动设备对扩展程序的支持较为有限。主流浏览器(如 Google Chrome)在任何操作系统上均不支持扩展程序,而苹果禁止非 Safari 浏览器在 iOS 系统上安装第三方扩展程序。不过,若使用特定浏览器,仍可在 Android 和 iOS 设备上安装浏览器扩展程序。移动设备对扩展程序 API 的支持情况不一。

移动端扩展程序用户界面

就 WebExtensions API 支持而言,允许安装扩展程序的移动浏览器通常与其桌面版本保持一致。弹出窗口(popups)、选项页面(options pages)、内容脚本(content scripts)和后台页面(background pages)均可正常运行和渲染。然而,由于移动设备的屏幕尺寸限制,需考虑以下用户体验差异:

  • 工具栏操作按钮:用于打开弹出窗口的按钮可通过浏览器的菜单按钮访问。
  • 选项页面:通常通过扩展程序管理视图中的“设置”按钮访问。用户较难找到此入口,因此不应将其视为常用入口。
  • Safari 扩展程序:以应用形式打包,包含额外的原生应用用户界面。本章后续将对此进行讨论。
  • 弹出窗口页面:在移动设备上,通常以独立网页(Firefox、Kiwi)或原生模态框(Safari)形式呈现。
  • 选项页面:始终以独立网页形式呈现。

Kiwi 浏览器

若希望在 Android 设备上安装 Chrome 扩展程序,最佳解决方案是使用 Kiwi 浏览器(图 15-7)。该浏览器基于 Webkit 和 Chromium,可直接从 Chrome 网上应用店安装扩展程序。但仅限在 Android 设备上的 Kiwi 浏览器中使用扩展程序。 15-7

Firefox 移动端

在 Firefox 市场上发布的扩展程序可选择支持 Firefox 移动端(如图 15-8 和 15-9 所示)。用户可直接从 Firefox 附加组件市场安装扩展程序。但需注意,Firefox 移动端浏览器上的扩展程序仅支持 Android 系统。

15-815-9

iOS Safari

Safari 扩展程序可通过 App Store 安装到 iOS 设备上。其结构与其他移动端扩展程序不同。为 iOS 发布 Safari 扩展程序需投入大量额外精力,但其核心仍采用与其他桌面扩展程序相同的 Web 架构(如图 15-10 和 15-11 所示)。

:本章后续将深入探讨 Safari 应用开发相关内容。 15-1015-11

自动化部署

截至 2022 年,所有主流扩展程序市场均支持自动化部署。本书未涵盖各市场的具体部署细节,但可参考以下文档链接:

提示:部分扩展程序开发平台(如 Plasmo,https://www.plasmo.com/)可自动化部署扩展程序至上述市场。

WebExtensions API 支持

并非所有浏览器均完全支持 WebExtensions API,且部分浏览器支持非标准化 API 功能。这些情况持续变化,因此本书仅提供以下页面链接,用于查阅各浏览器的 API 支持情况:

Safari 扩展程序开发

非 Safari 浏览器扩展程序仅包含原始 HTML、JS 和 CSS,可直接加载至浏览器。而 Safari 的浏览器扩展程序实现虽允许使用 HTML/JS/CSS 代码库,但在 Apple 设备上运行该代码库需额外投入大量工作。本节将介绍为 Safari 构建浏览器扩展程序所需的额外步骤。

:本节假设您正在从零开始构建 Safari 扩展程序,或将非 Safari 的 HTML/JS/CSS 扩展程序代码库移植到 Safari。

前置条件

Safari 浏览器扩展程序以应用程序形式开发并安装,需发布至 App Store。因此,其开发必须基于 Apple 的应用程序开发基础设施。要开发并发布 Safari 扩展程序,您需要以下内容:

  • 一台安装了 Xcode 的 Mac 电脑(用于开发)
  • 一个开发者证书(用于代码签名,费用为每年 99 美元)

架构

根据 Safari 开发者文档:
Safari 网页扩展程序由以下三部分组成,它们在各自的沙盒环境中独立运行:

  • 一个可包含用户界面的 macOS 或 iOS 应用程序
  • 在浏览器中运行的 JavaScript 代码和网页文件
  • 一个在 macOS 或 iOS 应用程序与 JavaScript 代码之间进行协调的原生应用扩展

这听起来可能有些复杂。为了更好地理解这些部分是如何协同工作的,让我们创建一个新的 Xcode 扩展项目,将其安装到 Apple 设备上,并逐一分析每个部分。

创建扩展程序项目

在 Xcode 中,使用“Safari Extension App”模板创建一个新项目。对于您的第一个项目,请选择 Swift 作为语言(图 15-12、15-13 和 15-14)。 15-1215-1315-14

注意:Xcode 模板生成的是清单(manifest)v2 版本的扩展程序,但 Safari 也支持清单 v3 版本。目前我们可以忽略这一差异。

让我们来看看生成的这些文件夹分别是什么:

  • iOS(应用)macOS(应用) 是操作系统将安装的正式应用程序。在 iOS 上,它会显示为主屏幕上的一个应用图标。这些应用的生成版本只是一个简单的 Web 视图。
  • Shared(应用) 是 Web 视图的内容。生成的版本仅包含默认图标、一些文本和一个按钮。
  • iOS(扩展)macOS(扩展) 是用于在 JavaScript 和原生应用之间进行协调的原生应用扩展。
  • Shared(扩展) 是正式的扩展程序代码,包括 manifest.json、JS/CSS/HTML、资源、本地化文件等。该目录还包含一个 ExtensionHandler.swift 文件,用于处理 JavaScript 和原生应用之间发送的消息。

注意:Xcode 为 iOS 和 macOS 分别设置了构建目标。这是合理的;尽管这些应用共享代码,但您是在为两个不同的平台开发两个完全独立的应用。

编写应用

生成的扩展程序默认情况下功能有限。让我们对其进行修改,以实现以下功能:

  • 使用清单 v3 版本
  • 在弹出窗口、后台脚本和内容脚本之间交换消息
  • 显示一个选项页面
  • 向原生应用发送消息

更新 Xcode 项目文件以匹配以下代码:

manifest.json

json
{
 "manifest_version": 3,
 "default_locale": "en",
 "name": "__MSG_extension_name__",
 "description": "__MSG_extension_description__",
 "version": "1.0",
 "icons": {
 "48": "images/icon-48.png",
 "96": "images/icon-96.png",
 "128": "images/icon-128.png",
 "256": "images/icon-256.png",
 "512": "images/icon-512.png"
 },
 "options_ui": {
 "page": "options.html"
 },
 "background": {
 "service_worker": "background.js"
 },
 "content_scripts": [{
 "js": [ "content.js" ],
 "matches": [ "<all_urls>" ]
 }],
 "action": {
 "default_popup": "popup.html",
 "default_icon": {
 "16": "images/toolbar-icon-16.png",
 "19": "images/toolbar-icon-19.png",
 "32": "images/toolbar-icon-32.png",
 "38": "images/toolbar-icon-38.png",
 "48": "images/toolbar-icon-48.png",
 "72": "images/toolbar-icon-72.png"
 }
 },
 "permissions": ["tabs", "nativeMessaging"],
 "host_permissions": ["<all_urls>"]
}

options.html

html
<!DOCTYPE html>
<html>
<head>
 <meta name="viewport" content="width=device-width">
 <meta charset="UTF-8">
</head>
<body>
 <h1>我是选项页面!</h1>
</body>
</html>

popup.html

html
<!DOCTYPE html>
<html>
<head>
 <meta charset="UTF-8">
 <link rel="stylesheet" href="popup.css">
 <script type="module" src="popup.js"></script>
</head>
<body>
 <h1>我是弹出窗口页面!</h1>
 <button id="set-bg-color">更改背景颜色</button>
 <button id="send-native-message">发送原生消息</button>
</body>
</html>

popup.js

javascript
document.querySelector("#set-bg-color").addEventListener("click", () => {
 const randomColor = "#" + Math.floor(Math.random()*(2 ** 24 - 1)).toString(16);
 chrome.tabs.query({
 active: true,
 currentWindow: true
 }, (tabs) => {
 chrome.tabs.sendMessage(
 tabs[0].id,
 { backgroundColor: randomColor },
 (response) => console.log(response)
 );
 });
});

document.querySelector("#send-native-message").addEventListener("click", () => {
 chrome.runtime.sendNativeMessage(
 "com.matt-frisbie.MVX",
 {msg: "foobar"},
 (response) => console.log(response)
 );
});

content.js

javascript
console.log('内容脚本已初始化!');
chrome.runtime.sendMessage({ greeting: "hello" }).then((response) => {
 console.log("内容脚本收到响应:", response);
});

chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
 console.log("内容脚本收到请求:", request);
 console.log("发送者:", sender);
 if (request.backgroundColor) {
 document.body.style.backgroundColor = request.backgroundColor;
 }
});

background.js

javascript
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
 console.log("后台收到请求:", request);
 console.log("发送者:", sender);
 if (request.greeting === "hello") {
 return sendResponse({ farewell: "goodbye" });
 }
});

示例 15-1. SafariWebExtensionHandler.swift

swift
import SafariServices
import os.log

let SFExtensionMessageKey = "message"

class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
 func beginRequest(with context: NSExtensionContext) {
 let item = context.inputItems[0] as! NSExtensionItem
 let message = item.userInfo?[SFExtensionMessageKey]
 os_log(.info, "MVX 应用收到:%{public}@", message as! CVarArg)
 let response = NSExtensionItem()
 response.userInfo = [ SFExtensionMessageKey: [ "App 响应给": message ] ]
 context.completeRequest(returningItems: [response], completionHandler: nil)
 }
}

以上代码大部分与本书其他部分的内容一致。总体而言,这是一个常规的后台/弹出窗口/选项/内容脚本扩展程序设置。代码中加粗的部分是用于从扩展程序的 JavaScript 代码向原生应用的 Swift 代码发送消息所需的部分。需要注意以下几点:

  • 需要特殊权限 nativeMessaging 才能发送原生消息。
  • 使用 chrome.runtime.sendNativeMessage 发送原生消息。
  • 要定位原生应用,必须使用原生应用的名称。对于 Safari 扩展程序,这是应用包标识符。在此示例中,标识符为 com.matt-frisbie.MVX

在 macOS 上进行测试

注意 若要在不签署应用程序的情况下开发 Safari 扩展,需要启用 Safari 加载未签名扩展的功能。为此,请选择 Safari > 偏好设置,点击高级,然后选择“在菜单栏中显示开发菜单”。接下来,打开开发菜单并勾选“允许未签名扩展”。

让我们先在桌面版 Safari 上进行测试。在 Xcode 中,为 macOS 构建应用程序(图 15-15)。 15-15

构建完成后,您将看到扩展应用程序已打开 (图 15-16)。 15-16

您所看到的视图是原生 macOS 应用程序展示的一个网页视图。 Xcode 会自动为 Safari 安装该扩展,因此请点击“退出并打开……”按钮以打开浏览器的偏好设置。 勾选“MVX”旁边的复选框以启用该扩展(图 15-17)。

15-17

注意: 此应用程序正在请求 <all_urls> 主机权限, 因此 Safari 会在启用该扩展之前要求您确认此权限。

点击“偏好设置”按钮,您的选项页面将会打开 (图 15-18)。

15-18

接下来,导航至一个有效的网页,例如 wikipedia.org。打开控制台, 以验证内容脚本是否正在与后台脚本完成消息握手 (图 15-19)。 15-19

默认的扩展程序图标是一个带圆圈的闪电符号,点击它可打开弹出页面。 点击“更改背景颜色”可将背景设置为随机颜色。 您应该会看到内容脚本记录了所接收到的消息 (图 15-20)。 15-20

第二个弹出按钮会向后台应用程序发送消息。若要查看后台应用程序的日志,您需要打开 macOS 的“控制台”应用程序,并筛选来自该扩展程序的消息。筛选来自 MVX 进程的消息,以及包含我们设置的示例“foobar”消息的消息。以下截图展示了点击“发送本地消息”后您应该会看到的日志输出(图 15-21)。 15-21

在 iOS 上测试

在 Xcode 中,为 iOS 构建应用程序,并使用模拟器(图 15-22)。 15-22

模拟器会直接启动原生应用程序(图 15-23)。
您所看到的视图是原生 iOS 应用程序展示的一个网页视图。 15-23

Xcode 会自动安装该扩展。您可以通过查看主屏幕来看到此应用程序(图 15-24)。 15-24

Safari 扩展应用可通过打开 Safari 设置并选择“扩展”来进行管理(图 15-25 和图 15-26)。 15-2515-26

点击“扩展设置”(Extension Settings)将会打开选项页面(如图15-27所示)。 15-27

接下来,导航到一个有效的网页,例如 wikipedia.org。当 iOS Safari 安装了扩展程序后,URL 栏会显示一个拼图图标。点击该图标以显示菜单,菜单中应包含一个 MVX 扩展程序选项(如图 15-28 所示)。 15-28

这个 MVX 选项的功能如同工具栏按钮,选择它将打开该扩展程序的弹出页面。点击“更改背景颜色”按钮,以测试内容脚本是否正常运行(如图 15-29 所示)。 15-29

你可以像在 macOS 的 Safari 浏览器中那样,通过“控制台”(Console)应用程序来跟踪点击“发送原生消息”(Send native message)按钮后生成的日志消息(如图 15-30 所示)。 15-30

部署到 App Store

你需要为 iOS 和 macOS 分别部署应用程序。部署流程与发布常规的 iOS 或 macOS 应用程序并无不同。从宏观层面来看,你需要执行以下步骤:

  1. 创建项目归档
  2. 构建并签名程序包
  3. 验证程序包
  4. 推送到 App Store 以开始审核流程

一个 macOS 部署流程的示例展示在图 15-31、15-32 和 15-33 中。 15-3115-3215-33

转换现有扩展

Xcode 内置了一个命令行工具,可以自动将现有的浏览器扩展代码库转换为 Safari 扩展。以下命令将生成一个包含你现有文件的 Xcode 扩展项目:

bash
xcrun safari-web-extension-converter /path/to/extension

提示:有关此 CLI 工具的更多详细信息,请访问:https://developer.apple.com/documentation/safariservices/safari_web_extensions/converting_a_web_extension_for_safari

Firefox 的特性差异

除了之前详细介绍的 Safari 扩展外,Firefox 插件生态系统在开发时存在一些值得注意的差异。

清单版本

在撰写本书时,Firefox 正处于过渡阶段。该组织正试图通过延长对 webRequestBlocking 等开发者希望保留的 API 的支持,来缓和从清单 v2 到 v3 的混乱过渡。这种情况可能不会持续太久,但目前请记住以下几点:

  • 当前情况:Firefox 是目前唯一一个默认不允许加载清单 v3 扩展的主要浏览器。虽然可以启用,但其支持并不稳定。
  • Webstore 情况:Firefox 是目前唯一一个不接受清单 v3 扩展的主要 Webstore。
  • 未来计划:Firefox 表示将采用清单 v3,但同时也会保留开发者仍然需要和使用的功能。最终结果尚不明确。

此外,当清单 v3 最终得到支持时,你需要在清单中添加一个插件 ID。MDN 文档中包含以下内容:

所有清单 v3 扩展在提交到 AMO(Add-ons Mozilla)时都需要在 manifest.json 中包含一个插件 ID。与清单 v2 扩展不同,AMO 不会接受没有 ID 的清单 v3 扩展,也不会自动将此 ID 嵌入到已签名的打包扩展中。

插件 ID 在 browser_specific_settings.gecko.id 下声明。例如:

json
manifest.json
{
  ...
  "browser_specific_settings": {
    "gecko": {
      "id": "browserextensionexplorer@buildingbrowserextensions.com"
    }
  },
  ...
}

注意:请阅读 Mozilla 的公告:https://blog.mozilla.org/addons/2022/05/18/manifest-v3-in-firefox-recap-next-steps/

侧边栏

Firefox 侧边栏是由扩展控制的容器,其定义方式与弹出窗口类似。它们在清单中通过 sidebar_action 键声明,并出现在活动网页的任一侧(图 15-34)。

15-34

API 扩展

Firefox 与其他浏览器不同,它在扩展 API 命名空间中添加了大量 API:

  • captivePortal
  • contextualIdentities
  • dns
  • find
  • menus
  • pkcs11(注意原文中的 pcks11 应为拼写错误,正确为 pkcs11
  • sidebarAction
  • theme
  • userScripts

提示:有关这些 API 差异的总结,请参阅此链接:https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Firefox_differentiators

总结

在本章中,我们涵盖了创建支持多浏览器的扩展的所有复杂方面。我们讨论了基于 Chrome Web Store 构建的扩展如何支持绝大多数基于 Chromium 项目的桌面浏览器。我们还讨论了所有可以部署浏览器扩展的不同位置,以及扩展在移动设备上的不同使用方式。

本章还涵盖了各种浏览器的特性差异。它详细描述了如何为 iOS 和 macOS 上的 Safari 开发和发布扩展,以及为 Firefox 开发的所有独特方面。

在下一章中,我们将涵盖所有不同的工具、平台和框架,这些工具、平台和框架可以使扩展开发和部署更加容易。