首页 / 浏览器扩展开发入门教程 / webextension-polyfill 桥接

浏览器扩展开发入门教程

webextension-polyfill 桥接

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

webextension-polyfill桥接跨浏览器browser-apichrome-api一套代码

本节目标:学完你能用 webextension-polyfill 把扩展里所有 API 调用统一成 browser.* 的 Promise 写法,让同一份代码在 Chrome 和 Firefox 上都能直接跑。

上一节说到,Chrome 用 chrome.* 回调、Firefox 用 browser.* Promise,命名空间和异步风格都不同。难道真要为每家浏览器写一套?不用。Mozilla 提供了一个官方桥接库,叫 webextension-polyfill,专门解决这件事。

52-1 polyfill 是什么,解决什么问题

webextension-polyfill 是 Mozilla 维护的一个小库(压缩后约 20KB)。它做的事很简单:在所有浏览器里都给你一个 browser 全局对象,并且这个 browser 上的异步方法一律返回 Promise。

原理是:它在底层检测当前环境。如果浏览器原生就有 Promise 化的 browser.*(如 Firefox),它基本原样透传;如果浏览器只有回调式的 chrome.*(如旧版 Chrome),它就包一层,把回调改写成 Promise。这样你在代码里只写 browser.*,不必关心跑在哪家浏览器。

Note

为什么不直接用 chrome.*?因为 Firefox 上 chrome.* 只对”和 Chrome 兼容”的 API 有效,且 Firefox 的 Promise 能力用不上。统一走 browser.* + polyfill,是 MDN 推荐、覆盖面最广的方案。

现状提示:从 Chrome 148 起,Chromium 已原生支持 browser 命名空间(Chrome 152 起覆盖全部扩展场景),Mozilla 官方也已在项目 README 中宣布 polyfill “has served its purpose”、不再接收新更新(冻结)。所以 polyfill 现在的定位是:兼容旧版 Chrome(148 以下)与统一写法的过渡方案;新项目也可以评估直接用原生 browser.*

52-2 引入 polyfill

引入方式有两种,按你的工程来选。

方式一:直接拷贝脚本(适合不用打包工具的小项目)。从 GitHub Releases 下载 browser-polyfill.min.js,放进扩展目录,然后在清单里让它在你的脚本之前加载。

注意一个约束:background.service_worker 只能指向单个入口文件。所以后台里,要把 polyfill 引入到入口最前面:脚本开了 "type": "module" 就用 import "./browser-polyfill.min.js"(副作用导入,脚本会在全局定义 browser),经典(非 module)脚本则用 importScripts('browser-polyfill.min.js');内容脚本是独立环境,把 polyfill 放在 content_scriptsjs 数组首位(content scripts 的 js 是数组,按顺序加载)。popup 等 HTML 页面则用 <script> 让 polyfill 先于业务脚本引入。

// background.js —— polyfill 必须最先执行
import "./browser-polyfill.min.js"; // 副作用导入:脚本在全局定义 browser
// 之后才是你的逻辑
browser.runtime.onInstalled.addListener(() => {
  console.log("background 就绪");
});

方式二:npm 安装(适合用 Vite 等构建工具)。直接装包,构建时一起打包。

npm install webextension-polyfill
// 在你的模块里
import browser from "webextension-polyfill";

注意:只有这种打包器场景才写 import browser from ...——npm 包有默认导出;直接拷贝的 browser-polyfill.min.js 没有默认导出,不能这样 import(见方式一)。

Tip

关键铁律:polyfill 必须在你自己的任何脚本之前执行。否则你的代码先去找 browser,在旧版 Chrome 里还不存在,就报错了。后台用 import/importScripts 放最前,内容脚本把 polyfill 放在 js 数组第一个,popup 等在 HTML 里用 <script> 先于业务脚本引入。

52-3 统一成 browser.* 写法

引入之后,全项目统一用 browser.*,再也不碰 chrome.*。这样代码在 Chrome、Firefox、Edge 上行为一致。

// 一份代码,三家浏览器通用
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
const tab = tabs[0];

await browser.tabs.sendMessage(tab.id, { kind: "highlight" });

try {
  await browser.storage.local.set({ lastTab: tab.id });
} catch (err) {
  console.error("存储失败", err);
}

消息响应也一样统一。注意一个坑:在 runtime.onMessage 的监听器里,如果你想异步返回响应,用 return Promise 的方式。Chrome 148+ 与 Firefox、Safari 都支持监听器返回 Promise 来异步回复;只有旧版 Chrome 才只认回调(需要 return true + sendResponse)。polyfill 在这里也做了兼容处理,帮你在旧版 Chrome 上把 Promise 翻译成回调式回复,所以统一返回 Promise 即可。

browser.runtime.onMessage.addListener((message, sender) => {
  if (message.kind === "getData") {
    return browser.storage.local.get("data"); // 返回 Promise 即异步回复
  }
});

52-4 一套代码跨浏览器的工程姿势

光有 polyfill 还不够,清单也可能要分别处理。常见做法是:维护一份”基础清单”,构建时用脚本生成各家变体。

// build-manifest.js(示意)
import base from "./manifest.base.json" with { type: "json" };

const firefox = {
  ...base,
  browser_specific_settings: { gecko: { id: "my-extension@example.com" } },
};
// Chrome 用 base 原样;Firefox 加 gecko id

构建工具(如 Vite、WXT、Plasmo)大多内置了”按目标浏览器产出清单”的能力,你只声明差异,工具拼装。这样源码一份,产物各浏览器一份,上传各自商店。

Note

用 polyfill 不等于”零差异”。它解决的是命名空间和异步风格;遇到某家浏览器根本不支持的 API,polyfill 也变不出来。这类”能力缺失”要在代码里做特性检测(下一节讲)。polyfill 负责”写法统一”,特性检测负责”能力兜底”。

52-5 常见失误

我列几个最容易踩的坑:

  • polyfill 没抢先加载:报 browser is not defined。检查加载顺序。
  • 内容脚本忘了引 polyfill:内容脚本是独立环境,得单独在 content_scripts.js 数组里第一位放 polyfill,或动态注入时一并注入。
  • 混用 chrome.*browser.*:选一个就坚持用。混用会让”统一”失去意义,也增加排查成本。
  • 以为 polyfill 能补齐缺失 API:它不能。Safari 不支持的 API,polyfill 也给不了。
// 反例:混用,别这么写
chrome.tabs.query({}, (tabs) => {});          // Chrome 风格
const t = await browser.tabs.query({});        // Firefox 风格
// 两行并存,既乱又容易出平台特定 bug

52-7 polyfill 与事件 API 的坑

polyfill 主要抹平”方法调用”的命名空间和异步风格,但事件 API 也有一处要当心。有些事件监听器允许你用返回值来异步回复(比如 runtime.onMessage),Chrome 148+ / Firefox / Safari 都支持 return Promise,只有旧版 Chrome 只认回调。polyfill 会检测环境,在旧版 Chrome 上把你的 Promise 翻译成回调式回复,所以你只管统一 return Promise

还有一类事件是”一次性注册、长期有效”的,比如 alarms.onAlarmruntime.onInstalled。它们不受 polyfill 影响,写法两边一样。判断标准就一条:只有”监听器要异步返回结果”的场景才需要担心 Promise 回复,普通监听直接做事即可。

// 普通监听:不需要 return,两边写法一致
browser.alarms.onAlarm.addListener((alarm) => {
  console.log("闹钟触发", alarm.name);
});

// 需要回复的监听:统一 return Promise,polyfill 兜底
browser.runtime.onMessage.addListener((msg) => {
  if (msg.kind === "ping") return Promise.resolve({ pong: true });
});

52-8 和 Vite 等构建工具配合

如果你用 Vite 打包扩展,polyfill 的最佳位置是在入口文件顶部 import。Vite 会把它和你的代码一起打包进各自的产物(background、content、popup)。别忘了在 manifest 配置里,把 polyfill 的引入逻辑保持”最先执行”——通常只要在入口模块第一行 import 即可,构建器会把它排在最前。

// 每个入口文件第一行
import browser from "webextension-polyfill";
// 接下来才是业务代码
Note

用构建工具时,不要同时用”拷贝脚本”和”npm 包”两种方式引 polyfill,否则会加载两份、体积翻倍还可能冲突。挑一种:小项目拷脚本、工程化项目用 npm 包。

52-9 小结

这一节你记三件事:

  1. webextension-polyfill 让所有浏览器都有 Promise 化的 browser.*
  2. 它必须最先加载;全项目统一写 browser.*,别混 chrome.*;需要回复的消息监听统一 return Promise
  3. 清单可用”基础 + 各浏览器差异”的方式分别产出,源码只一份;构建工具下在入口顶部 import 即可。

到这,命名空间和异步风格都统一了。但”统一写法”只是跨浏览器的第一关,真正的坑在”各家能力不一样”。下一节给一份兼容清单和排查思路。