什么是 WXT?

1767495559810.jpg

在第一章 Browser Extension Dev - 1. 介绍基本概念 里面,我使用了原始的 JavaScript 实现了一个简单的 Chrome 扩展。现在,我将使用 WXT 重写扩展。那么问题是:什么是 WXT?
简单来说,WXT 是用于浏览器扩展的开发框架,就像 Vite 是用于 Web 开发的流行框架一样。实际上,WXT 是基于 Vite 实现的,所以它也可以使用 Vite 插件的生态系统。在我看来,这是个非常棒的决定。

WXT 解决了什么问题?

那么,它能解决什么问题呢?

  1. 支持使用 TypeScript、Npm、React 等现代前端开发工具 – 对于熟悉现代 Web 工具链的开发者而言会感到宾至如归,不喜欢 React,也可以使用 Svelte,它不限制 UI 层框架
  2. 跨浏览器构建,主要的浏览器有 Chrome、Firefox 和 Safari – 是的,它对于跨浏览器扩展开发非常有用,打包多个 dist 轻而易举,从一开始提供的 API 就考虑到了跨浏览器构建
  3. 支持热更新,对于所有主流 Web 框架都已支持 – 你可能注意到之前每次修改扩展后都需要手动去浏览器刷新一下扩展加载修改,WXT 让这变得不再必要
  4. 提供了相对标准化的 message 通信、content script UI 注入方法 – 想要让注入的复杂 UI 和网页原本的 UI 互不干扰是个有挑战性的事情

如果想了解更详细的功能对比,可以参考官方文档:https://wxt.dev/guide/resources/compare.html

初始化项目

首先,让我们初始化一个项目,由于不涉及 UI 部分,所以只需要使用 vanilla 模版就好了。

1
2
3
pnpm dlx wxt@latest init 02-use-wxt --template vanilla --pm pnpm
cd 02-use-wxt
pnpm i

项目结构说明

现在我们得到一系列的目录和文件,让我们依次了解

1
2
3
4
5
6
7
wxt.config.ts # 扩展入口文件,定义 manifest 和构建流程,但通常不需要改动
entrypoints # 定义不同的入口点文件,例如 content script 或者 background 之类的,会自动写入到输出扩展的 manifest 中
public # 公共目录,会被原样复制到最终输出扩展的目录中,里面包含 icon 目录,用于放置扩展图标,会自动写入到输出扩展的 manifest 中
assets # 需要 bundle 的媒体资源目录,可以暂时忽略
components # 组件目录,通常放一些通用组件,例如 shadcn 之类的,可以暂时忽略
package.json
tsconfig.json

使用以下命令开发和构建

开发与构建流程

使用 pnpm dev 启动开发模式,输出目录在 .output/chrome-mv3-dev,在 Chrome 中需要加载这个目录作为扩展目录,而不是项目根目录。不过 WXT 会自动启动一个 Chrome 进程并自动加载扩展,所以可以不需要手动加载扩展。

1767442253914.jpg

1767442925701.jpg

但如果你不希望自动打开 Chrome 窗口,也可以配置 wxt.config.ts 来禁用这个行为。对于调试需要登录的网站而言,这也是必要的。

1
2
3
4
5
6
7
import { defineConfig } from 'wxt'

export default defineConfig({
webExt: {
disabled: true,
},
})

对于不熟悉现代前端工具链的人而言,开发和构建代码是分离的,不像早期那样编写的 JavaScript 就是运行在用户设备上的 JavaScript,使用 WXT 开发扩展也是一样的。

构建

使用 pnpm build 启动构建模式,输出目录在 .output/chrome-mv3,通常只有在需要调试 Firefox 或 Safari 版本时才需要使用构建后的扩展。

1767442265022.jpg

使用 pnpm zip 可以打包扩展的 zip 文件,对于 Firefox 还会有一个额外的 source 文件(Firefox AMO 要求提交扩展必须包含源码),这在提交到 Chrome Web Store 时才需要,这里先提一下。

1767442327565.jpg

实现扩展功能

设置 Manifest

在使用 WXT 之后,manifest 有许多部分都不再需要,它们通常都变成“约定配置”,不再需要手动处理。例如下面是之前实现扩展的 manifest.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"manifest_version": 3,
"name": "Hide AI Mode on Google Search",
"version": "0.0.1",
"description": "Hide the AI Mode button on Google Search pages.",
"content_scripts": [
{
"matches": ["https://www.google.com/"],
"js": ["content-scripts/content.js"],
"run_at": "document_start"
}
],
"icons": {
"128": "icon/128.png"
}
}
  1. manifest_version 会自动推导,Chrome/Safari 使用 v3,而 Firefox 默认使用 v2,不过我仍然建议统一使用 v3,以避免一些边缘情况需要兼容
  2. name/version/description 可以在 package.json 中配置,它会自动合并到输出目录中的 manifest.json。由于 package.json 的 name 字段有大小写限制,而且可能包含包名,所以我仍然建议在 wxt.config.ts 中配置 name/description 字段
  3. content_scripts/icons 字段完全不需要,它们会从 entrypoints 和 public/icon 目录自动推导出来

所以更新后的 wxt.config.ts 是

1
2
3
4
5
6
7
8
9
10
11
import { defineConfig } from 'wxt'

export default defineConfig({
manifestVersion: 3,
manifest: {
name: 'Hide AI Mode on Google Search',
version: '0.0.1',
description: 'Hide the AI Mode button on Google Search pages.',
},
// other config...
})

实现 Content Script

首先,还是让我们先清理一下无关文件。

1
rm -r ./assets ./components ./entrypoints/background.ts ./entrypoints/popup ./public/wxt.svg

接下来打开 entryponits/content.ts 文件,可以看到初始内容如下

1
2
3
4
5
6
export default defineContentScript({
matches: ['*://*.google.com/*'],
main() {
console.log('Hello content.')
},
})

而这里就是有趣的部分,defineContentScript 实际上定义了 manifest 和对应实际执行的脚本,这就是为什么上面在 wxt.config.ts 中省略 content_scripts 字段的原因,不过这也合理,将相关的代码和配置放在一起。将之前扩展的代码和配置修改过来之后变成:

1
2
3
4
5
6
7
8
9
10
export default defineContentScript({
matches: ['https://www.google.com/'],
runAt: 'document_start',
main() {
const style = document.createElement('style')
style.textContent =
'button:has([d="M15.65 11.58c.18-.5.27-1.03.31-1.58h-2c-.1 1.03-.51 1.93-1.27 2.69-.88.87-1.94 1.31-3.19 1.31C7.03 14 5 12.07 5 9.5 5 7.03 6.93 5 9.5 5c.46 0 .89.08 1.3.2l1.56-1.56C11.5 3.22 10.55 3 9.5 3 5.85 3 3 5.85 3 9.5S5.85 16 9.5 16c.56 0 2.26-.06 3.8-1.3l6.3 6.3 1.4-1.4-6.3-6.3c.4-.5.72-1.08.95-1.72z"]) { display: none; }'
document.documentElement.appendChild(style)
},
})

作为对比,之前的 content script 是放在 manifest.json 和 content-scripts/content.js 两个文件中完成。

1
2
3
4
5
6
7
8
9
{
"content_scripts": [
{
"matches": ["https://www.google.com/"],
"js": ["content-scripts/content.js"],
"run_at": "document_start"
}
]
}
1
2
3
4
5
// content-scripts/content.js
const style = document.createElement('style')
style.textContent =
'button:has([d="M15.65 11.58c.18-.5.27-1.03.31-1.58h-2c-.1 1.03-.51 1.93-1.27 2.69-.88.87-1.94 1.31-3.19 1.31C7.03 14 5 12.07 5 9.5 5 7.03 6.93 5 9.5 5c.46 0 .89.08 1.3.2l1.56-1.56C11.5 3.22 10.55 3 9.5 3 5.85 3 3 5.85 3 9.5S5.85 16 9.5 16c.56 0 2.26-.06 3.8-1.3l6.3 6.3 1.4-1.4-6.3-6.3c.4-.5.72-1.08.95-1.72z"]) { display: none; }'
document.documentElement.appendChild(style)

可以看到 WXT 的 content script 仍然是非常直观的,只是将下划线字段重命名为驼峰字段了(run_at => runAt)。在我们更新 content script 之后,直接打开 google.com 可以看到扩展已经热更新为了我们修改后的代码。扩展效果

1767490889790.jpg

注意:你可能注意到闪烁问题又出现了,这是因为 wxt 为了实现热更新加载机制,content script 是动态注入的,这导致 run_at: document_start 设置在开发模式下不太有用,但无需担心,在构建后它的工作正常。参考:https://github.com/wxt-dev/wxt/issues/357
提示:在开发过程中,如果需要查看 content script 的 console.log 输出,需要在网页 google.com(而不是扩展页面)打开 Devtools > Console 查看。

添加 icon

接下来,我们来添加扩展 icon。在 WXT 中,只需要将图标放到 ./public/icon/ 目录就好了,创建项目后可以看到自动创建了一些不同尺寸的图标,这是用于在显示时自动选择合适的图标,你可以选择使用任何工具来生成合适尺寸的图标,这里使用 ImageMagick 作为演示。

1
2
3
4
5
6
cd ./public
magick logo.png -resize 16x16 ./icon/16.png
magick logo.png -resize 32x32 ./icon/32.png
magick logo.png -resize 48x48 ./icon/48.png
magick logo.png -resize 96x96 ./icon/96.png
magick logo.png -resize 128x128 ./icon/128.png

如果更喜欢可视化工具,也可以使用 https://squoosh.app/ 来调整图片尺寸。或者使用 WXT 官方的 icon 模块 https://wxt.dev/auto-icons 来自动生成。

回到浏览器扩展管理页面,可以看到 icon 已经被正确识别了。

1767492255149.jpg

总结

现在,我们完成了第一个使用 WXT 实现的扩展就完成了,你觉得怎么样?在下一章中,我们将使用现代 Web 框架和 npm 包,为网页注入 UI 并实现更复杂的功能。

如果有任何问题,欢迎加入 Discord 群组讨论。
https://discord.gg/VxbAqE7gj2

完整代码:https://github.com/rxliuli/browser-extension-dev-examples/tree/main/packages/02-use-wxt

参考

WXT 官网 https://wxt.dev/