首页 / WXT 浏览器扩展框架教程 / WXT 背后干了啥:原理初探

WXT 浏览器扩展框架教程

WXT 背后干了啥:原理初探

本教程共 45 篇 · 第 6 篇 · 更新于 2026-08-13 · 约 3 分钟阅读

WXT原理manifest入口加载构建流程

本节目标:弄明白 WXT 构建期如何把入口文件变成 manifest 与最终产物,理解“入口顶层别乱用浏览器 API”这条铁律背后的原因。

一次构建发生了什么

运行 wxt build(或 dev)时,WXT 大致做四件事:

  1. 扫描 entrypoints/,发现所有入口点;
  2. 读取每个入口的配置选项(如内容脚本的 matches);
  3. 汇总生成对应浏览器的 manifest.json
  4. 用 Vite 把各入口打包成最终产物,输出到 .output/

第 2 步是关键:manifest 里的很多字段来自入口文件本身。比如内容脚本声明了 matches: ['*://*.wxt.dev/*'],这个值必须写进 manifest 的 content_scripts 声明,浏览器才知道往哪些网站注入。所以 WXT 必须“导入”你的入口文件,才能拿到这些选项。

难点:入口文件跑在 Node 里

HTML 入口好办,解析一下 meta 标签就拿到了选项。比如弹窗页可以这样声明 manifest 选项:

<!-- entrypoints/popup/index.html -->
<meta name="manifest.type" content="page_action" />

JS/TS 入口就麻烦了:WXT 是在 Node 环境里导入它们,而不是在浏览器里运行。浏览器专属的全局变量(windowdocumentchromebrowser)在 Node 里都不存在,直接导入必然报错。入口文件被导入后,加载器从默认导出里提取选项对象,这就是入口既能写配置又能写代码的原因。

WXT 用三步预处理来兜底:

  1. linkedom:提供最小一套浏览器全局对象(windowdocument 等);
  2. @webext-core/fake-browser:伪造一份扩展 API 的假实现,让 chrome/browser 全局存在;
  3. 代码预处理:把 main 函数从代码里剥离,再对剩余代码做 tree-shaking(摇树优化),去掉没被引用的部分。

剥离 main 的效果很巧妙:入口文件顶层的选项对象和 import 语句会保留,供提取配置;而 main 里的运行时代码在构建期不会执行,避免了副作用。

铁律:入口顶层别用浏览器 API

预处理并不完美:fake-browser 只实现了一部分 API,linkedom 也不等于真实浏览器。所以官方规则是——后台、内容脚本、未列出(unlisted)脚本等 JS/TS 入口,浏览器 API 只能写在 main 函数里

// ❌ 错误:顶层直接调用,构建期导入就会报错
browser.action.onClicked.addListener(() => {});

// ✅ 正确:放进 main 里,构建期不会执行
export default defineBackground(() => {
  browser.action.onClicked.addListener(() => {});
});

顶层写 document.createElement 之类的 DOM 操作同理。报错长这样:

ERROR  Browser.action.onClicked.addListener not implemented.
Tip

想看清预处理后的代码?跑 npx wxt prepare --debug,WXT 会把剥离后的代码打印出来,方便定位问题。

官方文档自己还在施工

官方有一页 How WXT Works,专门讲内部机制,但目前标注“施工中”(Under construction)。本节内容主要依据入口加载机制(Entrypoint Loaders)文档整理,原理细节以官方更新为准。想深入了解,可读 §07 之后的入口章节,那里会反复用到“配置在顶层、逻辑在 main”这个模型。

构建期导入入口文件时,WXT 用 fake-browser 兜住部分浏览器 API,但它只实现了一部分——所以规则很简单:运行时代码一律进 main。

小结

入口文件是 WXT 的“配置 + 代码”二合一:构建期在 Node 里导入取配置,运行时在浏览器里执行 main。理解这一步,就理解了框架为什么能自动生成 manifest,也记住了“浏览器 API 只进 main”的规矩。下一节开始逐个认识入口点的类型。