Chrome 与 Firefox 浏览器插件开发:核心差异与开发者指南


Chrome 和 Firefox 是目前桌面端最重要的两款浏览器,二者都实现了 WebExtensions API 标准,这让同一个扩展在大部分场景下可以“写一次,跑两处”。然而,由于历史包袱、安全策略和平台实现的不同,开发者在实际迁移和兼容过程中仍会遇到不少坑。

本文从架构、API、Manifest、发布审核和迁移实战等维度,系统梳理 Chrome 与 Firefox 扩展开发的核心差异,并提供可直接落地的兼容方案。

一、浏览器扩展架构概览

现代浏览器扩展通常由以下几部分组成:

  • Manifest 文件:扩展的入口配置,声明权限、后台脚本、内容脚本、图标等元信息。
  • 后台脚本(Background Script):扩展的生命周期脚本,处理事件、状态管理和跨页面通信。
  • 内容脚本(Content Script):注入到网页上下文中运行的脚本,可直接访问 DOM。
  • 弹出页面(Popup):点击工具栏图标时展示的临时页面。
  • 选项页(Options Page):扩展的设置页面。
  • 声明式规则(Declarative Rules):如 declarativeNetRequest,用于高效拦截和修改网络请求。

Chrome 在 Manifest V3 中强制使用 Service Worker 作为后台脚本,而 Firefox 仍保留了事件页面(Event Page)模式,这对需要长时间保持状态或频繁与页面交互的扩展影响较大。

二、核心差异速览

维度 Chrome Firefox
API 命名空间 chrome.* browser.*(同时兼容 chrome.*
异步处理 Manifest V3 原生支持 Promise,V2 主要使用回调 全面使用 Promise,更符合现代 JavaScript 规范
后台脚本 Manifest V3 强制使用 Service Worker 继续支持传统背景页面和事件页面
扩展签名 提交 Chrome 应用商店审核即可 所有正式扩展必须由 Mozilla 签名
隐私政策 相对宽松 严格执行“无意外”原则,数据收集需明确告知并获得用户同意
网络请求拦截 V3 使用 declarativeNetRequest,限制较多 仍支持更灵活的 webRequest 阻塞 API
CSS 注入 URL 解析 相对于目标页面解析 相对于被注入的 CSS 文件本身解析
内部资源 URL 使用扩展 ID 使用内部 UUID

三、API 层面的关键差异

3.1 命名空间:chromebrowser

Chrome 使用 chrome.* 命名空间访问所有扩展 API,而 Firefox 采用 browser.* 命名空间。不过 Firefox 为了兼容性,也支持 chrome.* 命名空间,因此许多 Chrome 扩展无需修改即可在 Firefox 中运行。

重要进展:从 Chrome 148 开始,Google 也开始支持 browser 命名空间。这意味着 browser.tabs.create({})chrome.tabs.create({}) 在 Chrome 中完全等效。这一变化旨在推动扩展 API 的标准化。

3.2 异步处理:回调与 Promise

异步处理风格是最影响开发体验的差异之一:

  • Firefox:所有异步 API 都返回 Promise,支持 async/await 风格。
  • Chrome(Manifest V2):使用回调函数,通过 chrome.runtime.lastError 处理错误。
  • Chrome(Manifest V3):大部分 API 已支持 Promise,但部分 API(如 devtools)仍不支持。
// Firefox / Chrome MV3(Promise 风格)
async function setExampleCookie() {
  try {
    const cookie = await browser.cookies.set({
      url: 'https://example.com/',
      name: 'session',
      value: 'abc123'
    });
    console.log('Cookie set:', cookie);
  } catch (error) {
    console.error('Failed to set cookie:', error);
  }
}

// Chrome MV2(回调风格)
chrome.cookies.set(
  { url: 'https://example.com/', name: 'session', value: 'abc123' },
  function (cookie) {
    if (chrome.runtime.lastError) {
      console.error(chrome.runtime.lastError);
    } else {
      console.log('Cookie set:', cookie);
    }
  }
);

3.3 API 覆盖范围差异

并非所有 API 在两个浏览器中完全一致,以下是几个典型例子:

  • Notifications API:Firefox 中 iconUrl 可选,Chrome 中必需;点击通知时 Firefox 会立即清除,Chrome 不会。
  • Proxy API:两个浏览器的代理 API 设计完全不兼容,跨浏览器代理类扩展通常需要两套实现。
  • 专有功能:Firefox 独有的 contextualIdentities(容器标签)API,Chrome 不支持。
  • 函数支持差异:例如 notifications.onButtonClicked 在 Firefox 中不支持,而 notifications.onShown 仅 Firefox 支持。
  • Web 请求拦截:Chrome V3 大幅限制了 webRequest 的阻塞能力,转而推广 declarativeNetRequest;Firefox 目前仍保留完整的 webRequest 阻塞能力。

3.4 Manifest V3 的兼容性

虽然主要浏览器已采用 Manifest V3,但实现细节仍有差异:

  • CSS URL 解析:Firefox 在处理注入 CSS 中的 URL 时,相对于 CSS 文件本身解析;Chrome 相对于页面解析。
  • Web 可访问资源:扩展中可访问的资源(web_accessible_resources)在不同浏览器中的 URL 格式也不同——Firefox 使用内部 UUID,Chrome 使用扩展 ID。
  • 后台脚本生命周期:Chrome V3 的 Service Worker 会在空闲时被终止,状态需要通过 chrome.storage 持久化;Firefox 的事件页面生命周期更宽松。

四、Manifest 文件对比

以下是一个同时兼容 Chrome 和 Firefox 的 Manifest V3 示例:

{
  "manifest_version": 3,
  "name": "Cross-Browser Demo Extension",
  "version": "1.0.0",
  "description": "A minimal extension that runs on both Chrome and Firefox.",
  "minimum_chrome_version": "148",
  "browser_specific_settings": {
    "gecko": {
      "id": "demo@example.com",
      "strict_min_version": "109.0"
    }
  },
  "permissions": [
    "storage",
    "tabs",
    "cookies",
    "activeTab"
  ],
  "host_permissions": [
    "https://example.com/*"
  ],
  "background": {
    "service_worker": "background.js"
  },
  "content_scripts": [
    {
      "matches": ["https://example.com/*"],
      "js": ["content.js"],
      "css": ["content.css"]
    }
  ],
  "action": {
    "default_popup": "popup.html",
    "default_icon": {
      "16": "icons/icon16.png",
      "48": "icons/icon48.png",
      "128": "icons/icon128.png"
    }
  },
  "web_accessible_resources": [
    {
      "resources": ["assets/*"],
      "matches": ["https://example.com/*"]
    }
  ]
}

关键字段说明

  • minimum_chrome_version: "148":确保用户使用的 Chrome 支持 browser 命名空间。
  • browser_specific_settings.gecko:Firefox 专用字段,用于声明扩展 ID 和最低版本。
  • 后台脚本:Firefox 支持 scripts 数组形式的背景脚本,而 Chrome V3 只接受 service_worker

如果需要同时支持 Firefox 的事件页面背景脚本,可以使用构建工具(如 webpack、rollup)根据目标浏览器输出不同的 Manifest。

五、跨浏览器兼容方案

5.1 使用 Polyfill 统一 API

Mozilla 提供的 WebExtension 浏览器 API Polyfill 是在 Chrome 中使用 browser.* 和 Promise 风格的首选方案。建议通过 npm 安装并打包进扩展,而不是手动复制文件:

npm install webextension-polyfill

在 Manifest V3 的 Service Worker 中,可通过 ES Module 引入 polyfill:

{
  "background": {
    "service_worker": "background.js",
    "type": "module"
  }
}
import browser from 'webextension-polyfill';

// 现在可以在 Chrome 中使用 browser.* + Promise
browser.tabs.query({ active: true, currentWindow: true })
  .then(tabs => console.log(tabs[0].url));

该 polyfill 会在支持 browser 命名空间的浏览器中自动跳过封装,在不支持的浏览器中提供兼容实现。对于内容脚本和弹出页面,同样只需 import browser from 'webextension-polyfill' 即可。

5.2 运行时命名空间切换

如果项目不想引入 polyfill,或需要兼容 Chrome 148 之前的版本,可以添加运行时防护代码:

if (typeof globalThis.browser === 'undefined') {
  globalThis.browser = chrome;
}

// 后续统一使用 browser.*
browser.storage.local.set({ key: 'value' });

但这种方式无法自动把回调风格 API 转换为 Promise,因此不如 polyfill 彻底。

5.3 特性检测而非浏览器嗅探

推荐使用特性检测来处理 API 差异:

async function setProxy(config) {
  if (browser.proxy && browser.proxy.settings) {
    // Firefox 风格
    await browser.proxy.settings.set({ value: config });
  } else if (chrome.proxy && chrome.proxy.settings) {
    // Chrome 风格
    await chrome.proxy.settings.set({ value: config });
  } else {
    console.warn('Proxy API not available');
  }
}

避免通过 navigator.userAgent 判断浏览器,因为 User-Agent 字符串可能被用户修改,且未来可能进一步精简。

六、发布与审核政策

Firefox 对扩展的审核比 Chrome 更严格,主要体现在以下几个方面:

6.1 “无意外”原则

扩展的功能必须与名称和描述一致。任何“意外”功能(如修改主页、搜索引擎、新标签页)必须明确告知用户,并取得主动同意(opt-in)。

6.2 数据收集

收集个人可识别信息(PII)前,必须获得用户明确同意,并在隐私政策中清晰说明收集的数据类型、用途和保留期限。

6.3 源代码提交

如果代码经过混淆、压缩或转译,必须向 Mozilla 提交原始源代码和构建说明,以便审核人员理解扩展行为。

6.4 签名要求

所有正式发布的 Firefox 扩展必须经过 Mozilla 签名。临时加载(about:debugging)仅用于开发测试。

6.5 发布流程对比

步骤 Chrome Web Store Firefox Add-ons(AMO)
注册费用 一次性 5 美元开发者注册费 免费
审核周期 通常数小时到几天 通常数小时到几天,复杂扩展可能更长
自动更新 通过 Chrome 自动更新机制 通过 Firefox 自动更新机制
版本管理 支持百分比发布 支持多渠道分发(推荐、自建等)
内联安装 已禁止 仅允许通过 AMO 安装

七、迁移实战:从 Chrome 到 Firefox

假设你有一个 Chrome Manifest V3 扩展,迁移到 Firefox 的推荐步骤如下:

  1. 验证 Manifest V3 支持:确认 Firefox 版本 >= 109,且目标 API 已被 Firefox 支持。

  2. 添加 Gecko 专属配置

    "browser_specific_settings": {
      "gecko": {
        "id": "your-extension@example.com"
      }
    }
  3. 引入 Polyfill:使用 webextension-polyfill 统一 API 调用。

  4. 检查后台脚本:如果使用了 Service Worker 专有特性(如 clients API),需要确认 Firefox 是否支持。

  5. 测试网络请求相关功能:如果扩展依赖 webRequestBlocking,在 Chrome V3 下可能需要重构。

  6. 准备隐私政策与源代码:为 AMO 审核准备清晰的隐私政策和构建说明。

  7. 使用 Firefox 开发者版测试:通过 about:debugging 临时加载扩展,充分测试后再提交审核。

常见迁移问题

  • 问题 1:Service Worker 在 Firefox 中行为不一致
    • 解决方案:尽量减少对 Service Worker 长生命周期的依赖,使用 chrome.storage 或 IndexedDB 持久化状态。
  • 问题 2:web_accessible_resources 路径解析不同
    • 解决方案:避免在 CSS 中使用相对路径的 url(),或在注入 CSS 时动态替换路径。
  • 问题 3:权限声明差异

八、调试技巧

Chrome

  1. 打开 chrome://extensions/
  2. 开启右上角“开发者模式”。
  3. 点击“加载已解压的扩展程序”,选择扩展目录。
  4. 点击 Service Worker 链接查看后台脚本日志。
  5. 在内容脚本运行的页面按 F12,在 Sources 面板中查看 Content Scripts。

Firefox

  1. 打开 about:debugging
  2. 选择“此 Firefox”或“临时加载附加组件”。
  3. 选择扩展的 Manifest 文件。
  4. 点击“检查”按钮打开 DevTools。
  5. Firefox 的 about:addons 页面可以查看扩展的权限和运行时信息。

九、总结

Chrome 和 Firefox 的扩展开发本质上是同源的,但差异集中在 API 命名空间、异步处理风格、后台脚本生命周期和发布政策四个方面。随着 W3C WebExtensions 社区组推动标准化,两者的差距正在缩小(如 Chrome 148 开始支持 browser 命名空间)。

对于开发者而言,实现高效跨浏览器开发的核心策略包括:

  • 使用 Mozilla 提供的 webextension-polyfill 统一 API 调用。
  • 查阅 MDN 兼容性表格,针对差异 API 做特性检测或分支处理。
  • 将 Firefox 作为跨浏览器扩展开发的起点,再移植到 Chrome。
  • 遵循各浏览器的发布与隐私政策,提前准备隐私政策和源代码说明。
  • 在两个浏览器中分别进行完整测试,尤其是后台脚本、网络请求和权限相关功能。

只要把握住这些核心原则,开发一个同时兼容 Chrome 和 Firefox 的高质量扩展并非难事。