一套代码跑所有浏览器
本教程共 45 篇 · 第 35 篇 · 更新于 2026-08-13 · 约 4 分钟阅读
本节目标:掌握指定目标浏览器(target browser)与 manifest 版本的方法,学会按浏览器过滤入口、配置 per-browser 选项,避开 Firefox 的兼容坑。
指定目标浏览器
WXT 默认按 Chrome 构建,用 -b(--browser)切换目标:
wxt # 等同 wxt -b chrome
wxt -b firefox
wxt -b custom
dev 模式下指定 Firefox 会打开 Firefox,其他字符串默认开 Chrome(浏览器二进制可配置,如 §05 所述)。Edge、Opera 这类 Chromium 系浏览器,都可以用 -b <名字> 构建,产物按「浏览器-manifest 版本」分目录存放,互不干扰(§38)。
日常开发可以给 package.json 配好双浏览器脚本,一键切换:
{
"scripts": {
"dev": "wxt",
"dev:firefox": "wxt -b firefox",
"build:firefox": "wxt build -b firefox"
}
}
运行时想区分浏览器,用内置环境变量(§27):
if (import.meta.env.BROWSER === 'firefox') {
// 仅 Firefox 构建执行
}
if (import.meta.env.FIREFOX) {
// 简写,等价于上面
}
指定 manifest 版本
--mv2 和 --mv3 决定构建哪个 manifest 版本。默认:Firefox 和 Safari 走 MV2,其余浏览器走 MV3。MV2 与 MV3 的差异不只版本号:MV3 的后台是 service worker,会休眠(§10);MV2 是常驻后台页,内存一直占着。写代码时如果依赖「后台常驻」,要意识到同一个扩展在不同浏览器上行为可能不一样。
运行时用 import.meta.env.MANIFEST_VERSION 读取目标版本:
if (import.meta.env.MANIFEST_VERSION === 2) {
// 仅 MV2 构建执行
}
按浏览器过滤入口
不是每个入口都适合所有浏览器。内容脚本用 include/exclude 控制:
export default defineContentScript({
include: ['firefox'], // 只在 Firefox 构建
main(ctx) {
// ...
},
});
HTML 入口用 meta 标签声明(§07 讲过 HTML 入口的 manifest 写法):
<meta name="manifest.exclude" content="['chrome']" />
嫌逐个入口写麻烦,还可以用配置里的 filterEntrypoints 直接列出允许构建的入口清单,一把梭:
// wxt.config.ts
export default defineConfig({
filterEntrypoints: ['popup', 'background'],
});
注意它和开发时的 -e 参数(§05)分工不同:-e 是临时只想看某几个入口,filterEntrypoints 是写进配置的固定清单,适合「这个版本只发这几个功能」的发布控制。
per-browser 选项
同一入口在不同浏览器要不同行为?选项可以按浏览器给对象,而不是单个值:
export default defineContentScript({
matches: {
chrome: ['*://chrome.example.com/*'],
firefox: ['*://firefox.example.com/*'],
},
runAt: {
chrome: 'document_start',
firefox: 'document_end',
},
world: {
firefox: 'MAIN',
},
});
匹配规则、注入时机、执行世界,都能逐浏览器定制,代码里不用写一堆 if。适合什么场景?比如某个网站只在 Chrome 上给了接口、Firefox 上要走另一套地址;或者只想在 Firefox 上使用主世界(Chrome 111 起、Firefox 128 起都支持 world: 'MAIN',§11 讲过内容脚本的执行世界),那就只在 Firefox 配置它。
manifest 按浏览器分支
manifest 本身也能按浏览器分支(§25)。真实项目的做法(mkext 模板):Firefox 分支里剥离 Chromium 专有字段,补上 gecko 配置:
// wxt.config.ts
export default defineConfig({
manifest: ({ browser }) => {
const isFirefox = browser === 'firefox';
const manifest = {
// ...公共字段
key: '...', // Chromium 专有
oauth2: { /* ... */ },
browser_specific_settings: isFirefox
? { gecko: { id: 'xxx@example.com' } }
: undefined,
};
if (isFirefox) {
const { key, oauth2, ...firefoxManifest } = manifest;
return firefoxManifest;
}
return manifest;
},
});
Firefox 的坑
key和oauth2是 Chromium 专有字段。Firefox 靠browser_specific_settings.gecko.id识别扩展,没有 Chrome 的identity.getAuthToken()(§21 讲过权限要按浏览器核实)。留着这些字段,AMO 审核会告警,还会白白暴露 client id;- favicon 接口(§23)只有 Chromium 系有,Firefox 里要绕过或降级;
- Firefox 走 AMO 审核,未知 manifest 字段、权限不符都会被点名;
- Safari 更特殊:默认 MV2,上架要走 Apple 的流程(§39),一般最后适配。
跨浏览器验收清单
一套代码跑所有浏览器,不是「能构建」就行,要每个目标都过一遍:
- dev 模式逐个浏览器跑一遍,入口、弹窗、内容脚本都点一遍;
- 构建后检查各产物目录的 manifest,确认 per-browser 字段与权限正确(§38);
- 重点核对「只有某浏览器才有的东西」:mkext 项目的做法是构建后直接检查产物——Chrome 包里保留
identity、oauth2、key,Firefox 包里确认这三样已剥离,browser_specific_settings.gecko已写入。
这步别省。商店审核是按浏览器分开审的,Chrome 过了不代表 Firefox 能过。发布流水线里也要把 chrome 和 firefox 的构建、打包都跑一遍(§39),任何一边失败都能在合并前发现,而不是等商店打回。
Note产物目录名里就带着目标:
.output/chrome-mv3/、.output/firefox-mv2/。构建完先看目录名,再看里面的 manifest,两处对不上说明构建目标搞错了。
小结
-b 选浏览器、--mv2/--mv3 选版本,入口与 manifest 按目标分支,Firefox 专坑提前绕。下一部分进入工程化:测试与发布。