首页 / 浏览器扩展开发入门教程 / Firefox WebExtensions 与 browser.*

浏览器扩展开发入门教程

Firefox WebExtensions 与 browser.*

本教程共 56 篇 · 第 51 篇 · 更新于 2026-08-13 · 约 7 分钟阅读

firefoxwebextensionsbrowser-apipromisemanifest-v3跨浏览器

本节目标:学完你能说清 Firefox 和 Chrome 在扩展 API 上最核心的差别——命名空间与异步风格,并知道 Firefox 在 MV3 下的兼容边界,为后面用 polyfill 写一套代码打底。

本教程主线一直用 chrome.*。但当你想把扩展也发到 Firefox,第一道坎就是:Firefox 不认 chrome.* 这套写法吗?它认,但它的”母语”是 browser.*。这一节把 Firefox 的扩展体系讲清楚。

51-1 Firefox 的命名空间:browser.* 不是 chrome.*

Chrome、Edge 这类 Chromium 内核浏览器,扩展 API 挂在 chrome 这个全局对象上。Firefox 不同,它把同一套能力挂在了 browser 这个全局对象上。

两套命名空间底下的方法名几乎一模一样。比如取当前标签页,Chrome 写 chrome.tabs.query,Firefox 写 browser.tabs.query。字段、参数、含义都相通,差别只在”前缀”。换句话说,迁移成本主要在调用风格,不在能力本身,这正好给 polyfill 留出了用武之地。

这里有个容易忽略的点:Firefox 同样兼容 chrome.*。对于和 Chrome 行为一致的 API,Firefox 会同时提供 browser.*chrome.* 两个入口。但官方建议、也是跨浏览器开发的事实标准,是用 browser.*。原因在下一节——它代表了一种更现代的异步风格。

// Chrome / Edge 写法
chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
  console.log(tabs[0].url);
});

// Firefox 的等效写法
browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => {
  console.log(tabs[0].url);
});

补一个最新现状:从 Chrome 148 起,Chromium 也原生支持 browser 命名空间了(Chrome 152 起覆盖全部扩展场景,含 devtools),“Chrome 只有 chrome.*”已不再是事实。不过 Chromium 里 browser.*chrome.* 目前仍是同一套实现,而 Firefox 的 browser.* 原生 Promise 化,写法差异依然存在——这正是后面 polyfill 要处理的。

Note

一句话总结:chrome.* 是 Chromium 的方言,browser.* 是 WebExtensions 标准提案的”普通话”。后面用 polyfill 时,我们就统一说”普通话”。

51-2 Promise 化的 API:告别回调

命名空间只是表面,真正改变写法的,是异步风格。

Chrome 的异步 API 传统上用回调函数:你把一个函数当参数传进去,结果在回调里拿。Firefox 的 browser.* 走的是 Promise:方法直接返回一个 Promise,你用 .then()await 拿结果。

看同一个例子对比。回调写法要把”拿到结果之后做什么”塞进一个函数;Promise 写法让异步流程像同步一样顺下来,尤其要连续调好几个 API 时,差距非常明显。

// Firefox:Promise 风格,可以 await
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
const tab = tabs[0];
const info = await browser.tabs.get(tab.id);
console.log(info.url);

错误处理也不一样。Chrome 回调里得查 chrome.runtime.lastError 判断有没有出错;Firefox 的 Promise 出错会直接 reject,你用 try/catch.catch() 接住就行。

// Firefox 用 try/catch 抓错误
try {
  const tab = await browser.tabs.create({ url: "https://example.com" });
  console.log("打开成功", tab.id);
} catch (err) {
  console.error("打开失败", err);
}
Tip

好消息是:从 Chrome 121 起,Chrome 的异步扩展 API 也普遍支持 Promise 了(少数例外如 devtools)。所以”Promise 化”正在变成全平台共识。MV3 之下,主流浏览器都朝返回 Promise 收敛。这让后面用 polyfill 统一写法更顺。

51-3 Firefox 的 MV3 兼容现状

你可能担心:Firefox 是不是还在旧一套后台模型上?放心,本教程只讲 MV3,Firefox 这边也是 MV3 路线。

Firefox 支持 manifest_version: 3。清单里的 actionbackground.service_workercontent_scriptspermissionshost_permissions 等字段,和 Chrome 基本一致。所以你前面学到的 MV3 知识,在 Firefox 上大多直接成立。

后台这块要特别注意一点。Firefox 的 MV3 后台同样用 background.service_worker 声明,运行模型是事件驱动、空闲就休眠——和 Chrome 的”服务工作者”思路一致。但 Firefox 对服务工作者的唤醒和休眠细节,跟 Chrome 不完全等价,比如有些 API 在 Firefox 后台里的可用时机略有差别。最稳妥的做法是:监听器写在脚本顶层、状态存进 storage、定时走 alarms,这套 V3 最佳实践对 Firefox 同样适用。

{
  "manifest_version": 3,
  "name": "我的跨浏览器扩展",
  "version": "1.0.0",
  "background": {
    "service_worker": "background.js"
  },
  "action": {
    "default_popup": "popup.html"
  }
}

顺带提一句,Firefox 还允许在 browser_specific_settings.gecko 里写 strict_min_version,声明扩展要求的最低 Firefox 版本。这样太老的 Firefox 会直接拒绝安装,能避免代码跑到不支持的 API 上而崩溃。这个字段在 Chrome 里会被忽略,所以你可以放心写进同一份清单,完全不影响 Chrome 加载。配合 id 一起用,是 Firefox 专属配置里最实用的两项。

51-4 browser_specific_settings 与 gecko id

Firefox 有一个 Chrome 没有的清单字段:browser_specific_settings。它用来放只有 Firefox(Gecko 内核)才认的配置,最常见的是给扩展一个固定 id。

{
  "browser_specific_settings": {
    "gecko": {
      "id": "my-extension@example.com"
    }
  }
}

这个 id 很有用。Firefox 里有些能力(比如用 nativeMessaging、或让扩展在不同版本间保持”同一个身份”)依赖一个稳定 id。如果你不写,Firefox 每次加载会临时生成一个,某些场景会出问题。

Note

browser_specific_settings 在 Chrome 里会被忽略,不会报错。所以你可以在一套清单里同时写 actionbrowser_specific_settings,Chrome 读前者、Firefox 读后者,互不打架。这正是”一套清单、多浏览器”的小技巧之一。

51-5 Firefox 侧边栏与少量差异点

清单字段层面,Firefox 和 Chrome 还有几处要留意:

  • 侧边栏:Chrome(MV3)用 side_panel,Firefox 用 sidebar_action——两者互不兼容(见 MDN 兼容表),sidebar_action 是 Firefox 现行的侧边栏机制,不是历史字段。跨浏览器清单需按目标浏览器分别声明。
  • options_ui:两边都有,行为接近。
  • 个别 API 覆盖度:Firefox 对 WebExtensions 标准的覆盖很广,但仍有少数 API 或方法在 Firefox 上不支持或行为不同。开发前查 MDN 的”Browser support for JavaScript APIs”兼容表最稳妥。
{
  "side_panel": {
    "default_path": "sidepanel.html"
  },
  "browser_specific_settings": {
    "gecko": { "id": "my-extension@example.com" }
  }
}

上面这个示例只声明了 side_panel,在 Firefox 里不会生效(Firefox 会忽略它);要支持 Firefox,需要在清单里另外声明 sidebar_action 并提供对应的侧边栏页面。

51-7 Firefox 的内容脚本与事件上下文

Firefox 的内容脚本在隔离世界(isolated world)里运行,这一点和 Chrome 一致:你的脚本读得到、改得了页面 DOM,却拿不到页面自己的全局变量。但 Firefox 对内容脚本生命周期的处理有细微差别,比如导航过程中内容脚本的存活时机、与页面脚本共享对象的限制,写之前建议回看”内容脚本”那个模块,把两边的边界对齐。

事件上下文也要留意。runtime.onMessage 的监听器可以直接 return 一个 Promise 来异步回复消息——Firefox 一直支持,Chrome 从 148 起也原生支持;只有旧版 Chrome 才需要 return true + sendResponse。polyfill 会统一处理这一点,你只管 return Promise,剩下的交给桥接库。

// Firefox 风格:监听器直接 return Promise 即异步回复
browser.runtime.onMessage.addListener(async (msg) => {
  if (msg.kind === "sum") {
    const data = await browser.storage.local.get("count");
    return { total: (data.count || 0) + 1 };
  }
});
Tip

如果发现消息回复”收不到”,先查监听器有没有正确 return Promise。在旧版 Chrome(148 以下)上,裸 chrome.* 的监听器还需要 return true + sendResponse,这也是 polyfill 出场前的典型踩坑点。

51-8 小结:为什么接下来要讲 polyfill

这一节你记三件事就够:

  1. Firefox 用 browser.* 命名空间,chrome.* 也兼容但非首选。
  2. browser.* 的异步 API 是 Promise 化的,用 await 比回调清爽。
  3. Firefox 的 MV3 和 Chrome 高度一致,但有 browser_specific_settings 等专属字段、内容脚本与事件上下文的少量差异。

但问题来了:你总不能写两遍代码,一份 chrome.* 回调、一份 browser.* Promise。下一节就讲 webextension-polyfill——它让一份代码在两边都能跑,而且统一用 browser.* 的 Promise 风格。