扩展 API 怎么调:wxt/browser 与类型安全
本教程共 45 篇 · 第 20 篇 · 更新于 2026-08-13 · 约 3 分钟阅读
本节目标:学会用统一的
browser变量调用扩展 API,弄懂类型从哪来、API 缺失时怎么检测,以及为什么入口点顶层不能碰浏览器 API。
各浏览器的 API 不统一
扩展能用的能力由浏览器提供,统称扩展 API(extension API)。麻烦在于各家给的入口不一样:Chrome 挂在全局 chrome 对象上,Firefox 挂在 browser 上;风格也不同,Chrome 老接口偏回调式,Firefox 偏 Promise 式。想一套代码跑所有浏览器,得有人先做统一。
统一入口:wxt/browser
WXT 把两家合并成一个变量,从 wxt/browser 导入:
import { browser } from 'wxt/browser';
browser.action.onClicked.addListener(() => {
// 用户点击了工具栏图标
});
browser 本质上是运行时浏览器提供的 browser 或 chrome 全局对象的直接导出。这意味着你可以放心写 Promise 风格 API,MV2、MV3 通用,Chromium、Firefox、Safari 都跑得动。
习惯写 chrome.* 的老代码要注意:Firefox 也提供 chrome.* 兼容命名空间,但支持不完整、行为风格各异,直接调容易踩坑。统一走 browser 最省心。
Tip默认开启的自动导入会帮你省掉 import 语句,直接写
browser就能用。自动导入机制见 §29。
如果你希望 browser 走 webextension-polyfill 的包装(统一所有 API 为 Promise 风格),可以安装 @wxt-dev/webextension-polyfill 并按它的安装指南配置。不装也没关系,默认实现已经够日常使用。
类型从哪来
WXT 的 browser 类型基于 @types/chrome。所有 API 类型集中在 Browser 命名空间里,写回调参数时直接引用:
import { type Browser } from 'wxt/browser';
function handleMessage(message: unknown, sender: Browser.runtime.MessageSender) {
// sender 携带来源标签页、框架等信息
}
WXT 会在 .wxt/ 目录里生成类型文件(如 §29 所述)。编辑器里悬停 browser,就能看到每个 API 的签名和注释;改完代码跑一次 wxt prepare,类型就保持最新。
特性检测:类型不会替你兜底
有些 API 受清单版本、浏览器和权限影响,运行时可能不存在。权限不足时,对应 API 是 undefined。类型系统假设所有 API 都存在,它帮不了你。判断 API 是否可用,要自己检测:
if (browser.runtime.onSuspend != null) {
browser.runtime.onSuspend.addListener(() => {
// 后台即将休眠
});
}
可选链写起来更简洁:
browser.runtime.onSuspend?.addListener(() => {
// ...
});
想兼容 MV2 和 MV3 里名字不同的同类 API,可以合并检测:
(browser.action ?? browser.browser_action).onClicked.addListener(() => {
// MV3 是 action,MV2 是 browser_action
});
给 Firefox 补类型
@types/chrome 没有 Firefox 独有 API 的类型,比如 sidebarAction。想补,先安装 @wxt-dev/browser,再用 TypeScript 声明合并(declaration merging)扩展 Browser 命名空间:
// <srcDir>/browser-types.d.ts
import '@wxt-dev/browser';
import type { SidebarAction } from 'webextension-polyfill';
declare module '@wxt-dev/browser' {
namespace Browser {
export const sidebarAction: SidebarAction.Static;
}
}
类型可以来自 webextension-polyfill、@types/firefox-webext-browser,也可以是你自己写的类型。
入口点顶层的坑
WXT 构建时会把入口文件先导入到 Node 环境(机制见 §06),那里没有浏览器提供的 chrome/browser 全局对象。WXT 用 fake-browser 兜了一层,但只实现了部分 API。所以在任何 JS/TS 入口点(后台、内容脚本、未列出脚本)的顶层调用浏览器 API,会直接报错:
✖ Command failed after 440 ms
ERROR Browser.action.onClicked.addListener not implemented.
修法很简单:把 API 调用挪进 main 函数。
// background.ts
browser.action.onClicked.addListener(() => { /* ... */ }); // 错:顶层
export default defineBackground(() => {
browser.action.onClicked.addListener(() => { /* ... */ }); // 对:main 里
});
常用套路
拿到扩展页面的 URL,用 runtime.getURL,别硬编码 chrome-extension://...:
const url = browser.runtime.getURL('/options.html');
await browser.tabs.create({ url });
查询当前活动标签页:
const [tab] = await browser.tabs.query({
active: true,
currentWindow: true,
});
能不能读到 url、title 字段,取决于上下文和权限。想为这个需求加权限前,先查浏览器实际规则。
监听安装事件,做一次性初始化:
export default defineBackground(() => {
browser.runtime.onInstalled.addListener(() => {
// 只执行一次的初始化
});
});
Note监听器要在后台启动时同步注册。别把它藏在一次可能还没完成的远程请求之后,否则事件会漏掉。
小结
- 统一从
wxt/browser导入browser,Promise 风格跨浏览器可用。 - 能力差异用特性检测,别硬编码浏览器判断。
- 浏览器 API 只能在入口的
main里调用(原理见 §06)。