WXT 尝试
此博文已迁移至 https://www.micais.me/posts/2025/06/issue-140
这两天看到了一个项目:WXT,简而言之,它就好比是扩展程序开发里的 Next.js。
我被它的几个特点吸引,于是将一个已经开发完成的扩展程序改为了使用 WXT,以下是体验报告。
先说结论
是否要使用取决于一点:你是否能忍受安装包体积变大。
不同项目可能体积变大的程度不一样,我自己的项目,体积变大了 30%。
在使用 WXT 之前,我使用 Webpack 打包我的源码,其中 background script 单独打包(target: 'webworker'),其余所有代码统一打包(target: 'web')。最后,打包出来的 zip 上传到 webstore 后,下载下来的 .crx 文件为 430KB。
使用 WXT 之后,.crx 文件为 607KB。变大的原因会在后面解释。
优点
我比较喜欢的几个点:
- 对内容脚本失效的情况(即
Error: Extension context invalidated.)做了检测,见文档- 但使用这个功能会要求内容脚本改为异步的,后面有说
- 基于文件系统的 entrypoint 定义,见文档
- 开发环境下会自动加载扩展程序并打开一个全新的浏览器 profile 用来测试,见文档
- 这可以把我自己使用的 Chrome 和用来测试扩展程序的 Chrome 分隔开来
- 开发环境下,修改代码后会自动刷新扩展 / 网页 / 侧边栏等
- 但是体验不太稳定,经常出现(大概是 2 天内出现了 3 次的频率)虽然自动刷新了,但实际上代码还是旧的,需要重启开发环境的情况
- 如果修改了环境变量,不会自动刷新,甚至重启开发服务也不会生效,需要修改用到了环境变量的文件代码(例如加一行
console.log('x'))才会应用到改动后的环境变量
- 开箱即用的多浏览器打包,见文档
- 但是我还没真的用过,所以体验如何未知
- 通过 CLI 发布扩展程序到各大 store,见文档
- 目前仅体验了自动发布到 Chrome Webstore,除了初次配置比较麻烦外(因为申请 Google 的 API Key 特别麻烦),之后体验都很好
缺点
内容脚本的 entrypoint 需要写成异步的
WXT 需要在 entrypoint 里使用 defineContentScript 函数来定义内容脚本,例如:
// entrypoints/example.content.ts
// 其余 import 可以像普通的场景一样使用
import other from './other'
export default defineContentScript({
matches: ['<all_urls>'],
main(ctx) {
// 但这个文件内的代码必须写在 main 里面
if(other.top) {
console.log('In top.')
}
},
});
这是我很不习惯的一点,当然也有 workaround:
// entrypoints/example.content.ts
// 所以逻辑都写在 bootstrap 里
import './bootstrap'
// entrypoint 仅用来定义内容脚本
export default defineContentScript({
matches: ['<all_urls>'],
});
WXT 应该是会剥离掉所有 import,然后在 Node.js 环境读取内容脚本的配置,因为经过测试,如果我在别的 js 文件顶层使用 self.top === self 这样的代码不会报错,但如果在 example.content.ts 里使用就会报错,需要放到 main 函数里。
另外,如果要用到优点里的“内容脚本失效检测”,那这也要求所有代码都改为在 main 里启动,这相当于强制内容脚本要用异步的方式来写,也就是所有代码都要包装进一个函数里。
安装包体积变大
上面的缺点都还算有 workaround,但这一个目前没有解决方案。
出现这个问题的原因是,WXT 的打包策略似乎是:
- 每个 content script 单独打包
- 其余所有部分:background、options、sidepanel 等统一打包
这意味着:内容脚本与内容脚本以及其它部分之间共同依赖的代码(例如 lodash、react)没有抽离出来公共 chunk。
我的情况是,我在两个内容脚本和 sidepanel 里都用了 react,最后我查看打包情况时,发现 react 没有被抽离出来,而是分别打包进了这 3 个 entroypoint。
这真的让我很难取舍
我向 WXT 提交了一个 issue,希望能有 workaround。
嗨,可以试试 WebExtend,它提供了很多与 WXT 相似的功能,关键是可以很好地解决上面的两个缺点。
同步的内容脚本
WebExtend 中,内容脚本是同步的,和原始的内容脚本写法无差异,没有 WXT 中的心智负担。示例如下。
// src/content.ts
import './index.css';
import type { ContentScriptConfig } from 'web-extend'
let root = document.getElementById('web-extend-content');
if (!root) {
root = document.createElement('div');
root.innerHTML = 'Hello World'
root.id = 'web-extend-content';
document.body.appendChild(root);
}
export const config: ContentScriptConfig = {
matches: ['https://developer.mozilla.org/*'],
};
极小的打包体积
WebExtend 使用 Rsbuild 作为打包器(Rsbuild 背后是 Rspack,兼容 Webpack),它自身提供了优秀的打包优化功能。
此外, WebExtend 将 content_scripts、options、popup 等统一打包,它们会共享 chunk,从而减少打包体积。
其他优点
- 基于文件系统的 entrypoint 定义,见文档
- dev 环境下会自动加载扩展程序并打开一个全新的浏览器 ,见文档
- dev 环境下支持 HMR 和自动刷新,并且支持内容脚本的热更新。
- 跨浏览器兼容,轻松实现多浏览器打包。
- 生成器支持
试用
# Node.js >= 18
npx web-extend@latest init