webextension-polyfill 桥接
本教程共 56 篇 · 第 52 篇 · 更新于 2026-08-13 · 约 7 分钟阅读
本节目标:学完你能用 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_scripts 的 js 数组首位(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.onAlarm、runtime.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 小结
这一节你记三件事:
webextension-polyfill让所有浏览器都有 Promise 化的browser.*。- 它必须最先加载;全项目统一写
browser.*,别混chrome.*;需要回复的消息监听统一return Promise。 - 清单可用”基础 + 各浏览器差异”的方式分别产出,源码只一份;构建工具下在入口顶部 import 即可。
到这,命名空间和异步风格都统一了。但”统一写法”只是跨浏览器的第一关,真正的坑在”各家能力不一样”。下一节给一份兼容清单和排查思路。