- 前端
- 开发工具
- 构建工具
- 插件系统
【免费下载链接】wxt
⚡ Next-gen Web Extension Framework
Entrypoints(入口点)是 WXT 扩展开发的核心概念——entrypoints/目录下的每一个文件或目录,都是 WXT 打包扩展的输入,也是 manifest.json 自动生成的依据。本篇指南基于 WXT 官方文档(docs/guide/essentials/entrypoints.md)与源码实现(find-entrypoints.ts),完整梳理入口点的目录规则、13 类入口点的文件名模式与可配置选项,并结合 wxt-demo 的真实示例,帮助你彻底掌握"声明式定义扩展"的 WXT 开发方式。
什么是 Entrypoints
在 WXT 中,entrypoints/目录是扩展的"装配车间":目录内的文件作为打包时的输入(inputs),参与构建产物生成。它们可以是 HTML、JS、CSS,也可以是 Vite 支持的任意变体——TypeScript(.ts)、JSX(.tsx/.jsx)、SCSS、Sass、Less、Stylus 等,均无需额外配置即可直接使用。
入口点不仅决定"构建什么",还决定"manifest 里写什么"。WXT 会读取每个入口点内声明的选项(manifest options),在构建时自动生成对应的 manifest.json,省去了手动维护background、content_scripts、action等清单字段的繁琐工作。
目录结构约定
在entrypoints/目录内,一个入口点既可以是单个文件,也可以是一个包含index文件的目录,两种写法等价:
📂 entrypoints/ 📄 {name}.{ext}📂 entrypoints/ 📂 {name}/ 📄 index.{ext}入口点的name直接决定它的类型。例如,要添加一个 Background 入口点,以下两种文件布局任选其一:
📂 entrypoints/ 📄 background.ts📂 entrypoints/ 📂 background/ 📄 index.ts从源码看,WXT 通过PATH_GLOB_TO_TYPE_MAP这张文件名 glob 模式映射表来识别入口点类型,见 find-entrypoints.ts。getEntrypointName则取相对路径中第一个.或/之前的片段作为入口点名称,见 entrypoints.ts。因此youtube.content.ts的名称是youtube、类型是 content-script,而example-tsx.content.tsx(见 wxt-demo)也能被正确识别。
在入口点目录中放置相关文件
使用目录形式entrypoints/{name}/index.{ext}时,可以在index文件旁边放置与该入口点配套的其它文件,它们会作为模块被正确打包,而不会被误认为新入口点:
📂 entrypoints/ 📂 popup/ 📄 index.html ← 这是入口点 📄 main.ts 📄 style.css 📂 background/ 📄 index.ts ← 这是入口点 📄 alarms.ts 📄 messaging.ts 📂 youtube.content/ 📄 index.ts ← 这是入口点 📄 style.css禁止把相关文件直接放进 entrypoints/ 根目录
千万不要把某个入口点的附属文件直接放在entrypoints/目录下——WXT 会把它们当成独立入口点尝试构建,通常会直接报错。正确做法是把这些文件放进对应入口点的目录中:
📂 entrypoints/ 📄 popup.html ✗ 错误:应使用目录 📄 popup.ts ✗ 错误 📄 popup.css ✗ 错误 📂 popup/ ✓ 正确 📄 index.html 📄 main.ts 📄 style.css不支持深层嵌套
entrypoints/目录虽然在观感上类似 Nuxt 或 Next.js 的pages/目录,但WXT 不支持同样的深层嵌套。入口点只能位于entrypoints/下零层或一层(即entrypoints/{name}.{ext}或entrypoints/{name}/index.{ext}),嵌套更深将无法被发现和构建:
📂 entrypoints/ 📂 youtube/ ✗ 错误:嵌套过深 📂 content/ 📄 index.ts 📄 ... 📂 injected/ 📄 index.ts 📄 ... 📂 youtube.content/ ✓ 正确:用命名后缀区分 📄 index.ts 📄 ... 📂 youtube-injected/ ✓ 正确:用命名后缀区分 📄 index.ts 📄 ...正如PATH_GLOB_TO_TYPE_MAP所示,WXT 只匹配entrypoints/下一层的 glob 模式(如*.content.[jt]s?(x)、*/index.html),更深层的路径不在匹配范围内,这就是嵌套不被支持的根源。
Listed 与 Unlisted:两类入口点
Web 扩展中存在两种入口点:
- Listed(已列出):被引用在
manifest.json中的入口点,例如 Popup、Options、Background、Content Script 等。WXT 文档中通常直接用名字称呼它们。 - Unlisted(未列出):不出现在 manifest 中,但扩展运行时会用到的入口点,例如:
- 扩展安装后在新标签页展示的欢迎页;
- 由内容脚本注入到主世界(main world)的 JS 文件。
Unlisted 入口点的具体用法见下文 Unlisted Pages、Unlisted Scripts 与 Unlisted CSS。
在入口点内部定义 manifest 选项
大多数 listed 入口点需要在manifest.json中声明对应选项。与"另起一个文件维护 manifest"的传统方式不同,WXT 把选项直接定义在入口点文件内部:
- 对 JS 类入口点,选项作为
defineXxx工厂函数的参数传入。例如给内容脚本声明matches:
export default defineContentScript({ matches: ['*://*.wxt.dev/*'], main() { // ... }, });- 对 HTML 类入口点,选项通过
<meta>标签配置。例如为 MV2 popup 使用page_action:
<!doctype html> <html lang="en"> <head> <meta name="manifest.type" content="page_action" /> </head> </html>构建时,WXT 会收集入口点中声明的选项,据此生成 manifest.json。
meta 标签的解析机制(源码级)
从源码看,HTML 入口点的选项解析逻辑位于 find-entrypoints.ts 的importHtmlEntrypoint函数:
- 只处理
name以manifest.或wxt.前缀开头的<meta>标签,其余标签直接忽略; manifest.前缀后的内容会通过camelCase转为驼峰键名(例如manifest.default_icon→defaultIcon);<meta>的content属性会优先尝试用 JSON5 解析成结构化数据(这就是 Popup 的default_icon能写成对象字面量、theme_icons能写成数组的原因),解析失败才退化为原始字符串;<title>标签的内容也会被读取,作为title选项(例如 Popup 的default_title)。
按浏览器差异化配置选项
WXT 的入口点选项还支持per-browser(按浏览器)差异化:任何选项都可以写成{ chrome: ..., firefox: ... }这种按目标浏览器取值的形式。源码中的resolvePerBrowserOption/resolvePerBrowserOptions(见 entrypoints.ts)会在解析时根据当前构建目标浏览器替换成对应值,其中defaultIcon是唯一的特例——它是 Record 结构,被显式排除在解析之外,以避免与 per-browser 语法冲突。这意味着你可以在同一个入口点里为 Chrome 与 Firefox 声明不同的runAt、persistent等行为。
Entrypoint 类型详解
下面按类型逐一说明文件名模式、可配置选项与注意事项。文件名模式中的[jt]sx?表示.js、.ts、.jsx、.tsx均可用;[jt]s表示.js或.ts。
Background
文件名模式:
background.[jt]s→ 输出为background.jsbackground/index.[jt]s→ 输出为background.js
最小写法:
export default defineBackground(() => { // 后台脚本加载时执行 });带 manifest 选项的写法:
export default defineBackground({ // 设置 manifest 选项 persistent: undefined | true | false, type: undefined | 'module', // 设置在部分浏览器构建中是否移除该入口点 include: undefined | string[], exclude: undefined | string[], main() { // 后台脚本加载时执行,注意:不能是 async }, });关键行为:
- 在 MV2 中,background 作为脚本挂到后台页面(background page);在 MV3 中,background 成为 Service Worker。
- 构建过程中,WXT 会在 Node.js 环境中导入该文件以读取配置,因此任何运行时逻辑都不能写在
main函数之外。下面这种写法是错误的:
browser.action.onClicked.addListener(() => { // ✗ 错误:模块顶层代码会在 Node 构建环境执行 // ... }); export default defineBackground(() => { browser.action.onClicked.addListener(() => { // ✓ 正确:放进 main 内 // ... }); });源码层面,defineBackground是一个纯类型包装函数(见 define-background.ts):传入函数时包装成{ main: fn },传入对象时原样返回。类型定义BackgroundDefinition明确要求main(): void(见 types.ts),即 background 的 main 不允许是异步的。WXT 的加载机制详见 Entrypoint Loaders。
Bookmarks
文件名模式:
bookmarks.html→ 输出为bookmarks.htmlbookmarks/index.html→ 输出为bookmarks.html
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Title</title> <!-- 设置在部分浏览器构建中是否移除该页面 --> <meta name="manifest.include" content="['chrome', ...]" /> <meta name="manifest.exclude" content="['chrome', ...]" /> </head> <body> <!-- ... --> </body> </html>定义 Bookmarks 入口点后,WXT 会自动修改 manifest,用你的 HTML 页面覆盖浏览器的书签管理页。
Content Scripts
文件名模式:
content.[jt]sx?→ 输出为content-scripts/content.jscontent/index.[jt]sx?→ 输出为content-scripts/content.js{name}.content.[jt]sx?→ 输出为content-scripts/{name}.js{name}.content/index.[jt]sx?→ 输出为content-scripts/{name}.js
注意:命名内容脚本({name}.content.ts)是内容脚本推荐的组织方式,多个内容脚本不会互相覆盖。
export default defineContentScript({ // 设置 manifest 选项 matches: string[], excludeMatches: undefined | [], includeGlobs: undefined | [], excludeGlobs: undefined | [], allFrames: undefined | true | false, runAt: undefined | 'document_start' | 'document_end' | 'document_idle', matchAboutBlank: undefined | true | false, matchOriginAsFallback: undefined | true | false, world: undefined | 'ISOLATED' | 'MAIN', // 设置在部分浏览器构建中是否移除该入口点 include: undefined | string[], exclude: undefined | string[], // 配置 CSS 注入页面的方式 cssInjectionMode: undefined | "manifest" | "manual" | "ui", // 配置内容脚本的注册方式 registration: undefined | "manifest" | "runtime", main(ctx: ContentScriptContext) { // 内容脚本加载时执行,可以是 async }, });关键行为:
- 与 background 相同,构建时该文件也会在 Node.js 环境被导入,所以运行时代码必须放进
main:
const container = document.createElement('div'); // ✗ 错误 document.body.append(container); // ✗ 错误 export default defineContentScript({ main: function () { const container = document.createElement('div'); // ✓ 正确 document.body.append(container); // ✓ 正确 }, });defineContentScript同样是类型包装函数(见 define-content-script.ts),运行时不做任何处理。- 在 wxt-demo 中可以看到
content.ts、iframe.content.ts、location-change.content.ts、main-world.content.ts、example-tsx.content.tsx、automount.content/、ui.content/、injected.content/等多个内容脚本实例(见 entrypoints 目录)。 - 内容脚本 UI 的创建方式与 CSS 注入配置,详见 Content Scripts 指南。
Devtools
文件名模式:
devtools.html→ 输出为devtools.htmldevtools/index.html→ 输出为devtools.html
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <!-- 设置在部分浏览器构建中是否移除该页面 --> <meta name="manifest.include" content="['chrome', ...]" /> <meta name="manifest.exclude" content="['chrome', ...]" /> </head> <body> <!-- ... --> </body> </html>Devtools 入口点用于注册浏览器的开发者工具面板;如需添加不同的面板(panels)与窗格(panes),可以参考 WXT 官方的 devtools-extension 示例项目。
History
文件名模式:
history.html→ 输出为history.htmlhistory/index.html→ 输出为history.html
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Title</title> <!-- 设置在部分浏览器构建中是否移除该页面 --> <meta name="manifest.include" content="['chrome', ...]" /> <meta name="manifest.exclude" content="['chrome', ...]" /> </head> <body> <!-- ... --> </body> </html>定义 History 入口点后,WXT 会自动修改 manifest,用你的 HTML 页面覆盖浏览器的历史记录页。
Newtab
文件名模式:
newtab.html→ 输出为newtab.htmlnewtab/index.html→ 输出为newtab.html
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Title</title> <!-- 设置在部分浏览器构建中是否移除该页面 --> <meta name="manifest.include" content="['chrome', ...]" /> <meta name="manifest.exclude" content="['chrome', ...]" /> </head> <body> <!-- ... --> </body> </html>定义 Newtab 入口点后,WXT 会自动修改 manifest,用你的 HTML 页面覆盖浏览器的新标签页。
Options
文件名模式:
options.html→ 输出为options.htmloptions/index.html→ 输出为options.html
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Options Title</title> <!-- 自定义 manifest 选项 --> <meta name="manifest.open_in_tab" content="true|false" /> <meta name="manifest.chrome_style" content="true|false" /> <meta name="manifest.browser_style" content="true|false" /> <!-- 设置在部分浏览器构建中是否移除该页面 --> <meta name="manifest.include" content="['chrome', ...]" /> <meta name="manifest.exclude" content="['chrome', ...]" /> </head> <body> <!-- ... --> </body> </html>open_in_tab决定选项页是在新标签页打开还是嵌入在扩展管理界面中;chrome_style/browser_style则分别对应 Chrome 与 Firefox 的浏览器内置样式。wxt-demo 中的 options 入口点 就是该模式的完整示例(配套main.ts与style.css)。
Popup
文件名模式:
popup.html→ 输出为popup.htmlpopup/index.html→ 输出为popup.html
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <!-- 设置 manifest 中的 `action.default_title` --> <title>Default Popup Title</title> <!-- 自定义 manifest 选项 --> <meta name="manifest.default_icon" content="{ '16': '/icon-16.png', '24': '/icon-24.png', ... }" /> <meta name="manifest.type" content="page_action|browser_action" /> <meta name="manifest.browser_style" content="true|false" /> <!-- 仅 Firefox:设置动作按钮的放置位置 --> <meta name="manifest.default_area" content="navbar|menupanel|tabstrip|personaltoolbar" /> <!-- 仅 Firefox:亮色/暗色主题下的图标 --> <meta name="manifest.theme_icons" content="[ { light: '/icon-light-16.png', dark: '/icon-dark-16.png', size: 16 }, { light: '/icon-light-32.png', dark: '/icon-dark-32.png', size: 32 } ]" /> <!-- 设置在部分浏览器构建中是否移除该页面 --> <meta name="manifest.include" content="['chrome', ...]" /> <meta name="manifest.exclude" content="['chrome', ...]" /> </head> <body> <!-- ... --> </body> </html>Popup 是扩展工具栏弹出窗,上述选项最终都会映射到 manifest 的action(MV3)或browser_action/page_action(MV2)字段。源码中getPopupEntrypoint(见 find-entrypoints.ts)会做额外的规整:title被重命名为defaultTitle,type被映射为actionType(非page_action一律视为browser_action),并通过 getter/setter 保持与旧字段mv2Key的同步兼容。wxt-demo 的 popup.html 是真实示例。
Sandbox
警告:仅 Chromium 支持沙箱页面(sandboxed pages),Firefox 不支持。
文件名模式:
sandbox.html→ 输出为sandbox.htmlsandbox/index.html→ 输出为sandbox.html{name}.sandbox.html→ 输出为{name}.html{name}.sandbox/index.html→ 输出为{name}.html
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Title</title> <!-- 设置在部分浏览器构建中是否移除该页面 --> <meta name="manifest.include" content="['chrome', ...]" /> <meta name="manifest.exclude" content="['chrome', ...]" /> </head> <body> <!-- ... --> </body> </html>wxt-demo 中提供了 sandbox.html 与目录形式的 example.sandbox 两个示例。命名沙箱({name}.sandbox)常用于为扩展提供不受扩展 CSP 限制的 iframe 运行环境。
Side Panel
文件名模式:
sidepanel.html→ 输出为sidepanel.htmlsidepanel/index.html→ 输出为sidepanel.html{name}.sidepanel.html→ 输出为{name}.html{name}.sidepanel/index.html→ 输出为{name}.html
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Default Side Panel Title</title> <!-- 自定义 manifest 选项 --> <meta name="manifest.default_icon" content="{ '16': '/icon-16.png', '24': '/icon-24.png', ... }" /> <meta name="manifest.open_at_install" content="true|false" /> <meta name="manifest.browser_style" content="true|false" /> <!-- 设置在部分浏览器构建中是否移除该页面 --> <meta name="manifest.include" content="['chrome', ...]" /> <meta name="manifest.exclude" content="['chrome', ...]" /> </head> <body> <!-- ... --> </body> </html>在 Chrome 中,侧边栏使用side_panelAPI;在 Firefox 中则使用sidebar_actionAPI。WXT 会根据目标浏览器自动生成对应的 manifest 字段。wxt-demo 中的 sidepanel.html 可作参考。
Unlisted CSS
文件名模式:
{name}.(css|scss|sass|less|styl|stylus)→ 输出为{name}.css{name}/index.(css|scss|sass|less|styl|stylus)→ 输出为{name}.csscontent.(css|scss|sass|less|styl|stylus)→ 输出为content-scripts/content.csscontent/index.(css|scss|sass|less|styl|stylus)→ 输出为content-scripts/content.css{name}.content.(css|scss|sass|less|styl|stylus)→ 输出为content-scripts/{name}.css{name}.content/index.(css|scss|sass|less|styl|stylus)→ 输出为content-scripts/{name}.css
body { /* ... */ }CSS 入口点始终是 unlisted(不会进入 manifest 的content_scripts声明)。如需为内容脚本注入 CSS,请参考 Content Scripts 文档中的 CSS 章节。使用 SCSS、Less 等预处理器时,按 Vite 官方指南配置对应预处理器依赖即可,WXT 无需额外配置。wxt-demo 中 injected.content/index.css、example-2.scss 都是 CSS 入口点的实例。
Unlisted Pages
文件名模式:
{name}.html→ 输出为{name}.html{name}/index.html→ 输出为{name}.html
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Title</title> <!-- 设置在部分浏览器构建中是否移除该页面 --> <meta name="manifest.include" content="['chrome', ...]" /> <meta name="manifest.exclude" content="['chrome', ...]" /> </head> <body> <!-- ... --> </body> </html>运行时,unlisted pages 通过/{name}.html路径访问:
const url = browser.runtime.getURL('/{name}.html'); console.log(url); // "chrome-extension://{id}/{name}.html" window.open(url); // 在新标签页打开该页面这非常适合"安装后欢迎页"之类的场景。注意{name}需要替换成你实际的文件名,例如welcome.html。
Unlisted Scripts
文件名模式:
{name}.[jt]sx?→ 输出为{name}.js{name}/index.[jt]sx?→ 输出为{name}.js
最小写法:
export default defineUnlistedScript(() => { // 脚本加载时执行 });带选项的写法:
export default defineUnlistedScript({ // 设置在部分浏览器构建中是否移除该脚本 include: undefined | string[], exclude: undefined | string[], main() { // 脚本加载时执行 }, });运行时,unlisted scripts 通过/{name}.js路径访问:
const url = browser.runtime.getURL('/{name}.js'); console.log(url); // "chrome-extension://{id}/{name}.js"你需要在需要的地方自行加载/运行这些脚本。如果脚本要注入到网页中(如内容脚本向主世界注入),别忘了把脚本及其相关资源加入 manifest 的web_accessible_resources,否则网页环境无法访问它们。
与其他 JS 入口点一致,构建时该文件会在 Node.js 环境被导入,因此运行时代码必须放在main内:
document.querySelectorAll('a').forEach((anchor) => { // ✗ 错误:顶层代码 // ... }); export default defineUnlistedScript(() => { document.querySelectorAll('a').forEach((anchor) => { // ✓ 正确 // ... }); });defineUnlistedScript的实现与defineBackground完全对称(见 define-unlisted-script.ts):函数参数会被包装成{ main: fn }。wxt-demo 中的 unlisted.ts 是实际示例。
构建时的校验与跳过机制(源码级)
从 find-entrypoints.ts 可以还原 WXT 构建入口点的完整流程:
- 发现:用
tinyglobby按PATH_GLOB_TO_TYPE_MAP的键在entrypoints/下做 glob 匹配,得到候选文件列表; - 去重:如果
{name}/index.{ext}与{name}/index.html同时存在,非 HTML 的 index 文件会被过滤掉; - 校验:
preventNoEntrypoints在目录为空时报错No entrypoints found in ...;preventDuplicateEntrypointNames在出现同名入口点时抛出 "Multiple entrypoints with the same name detected"(同名可能来自popup.html与popup/index.html这类冲突,见 find-entrypoints.ts); - 读取选项:HTML 入口点走
importHtmlEntrypoint解析<meta>与<title>;JS 入口点通过wxt.builder.importEntrypoints在构建器(Vite)中导入并读取默认导出;CSS 入口点没有选项; - 组装:按类型分别调用
getPopupEntrypoint、getBackgroundEntrypoint、getContentScriptEntrypoint等工厂函数,生成带输出目录与规范化选项的入口点对象;内容脚本的产物统一输出到content-scripts/子目录; - dev 兜底:在
wxt serve(开发模式)下如果没有 background 入口点,会自动注入一个 noop(空操作)background,保证扩展能正常加载(见 find-entrypoints.ts); - 跳过判定:
isEntrypointSkipped依据include/exclude选项判断当前目标浏览器是否应跳过该入口点——注意include与exclude不能同时使用,同时声明会打印警告并将该入口点标记为跳过(见 find-entrypoints.ts)。这也是文档中所有类型都提供include/excludemeta 选项的原因:同一份代码可以按['chrome']、['firefox']等目标浏览器裁剪构建产物。
总结
WXT 的 entrypoints 机制把"文件系统即配置"的思想贯彻到了扩展开发中:
- 目录约定决定了入口点的类型与产物路径:零层或一层的文件布局、
{name}.{type}的命名后缀(.content、.sandbox、.sidepanel等)是关键; - 选项内联让 manifest 与入口点代码同处一地:JS 入口点用
defineBackground/defineContentScript/defineUnlistedScript声明,HTML 入口点用manifest.*前缀的<meta>标签声明,并天然支持按浏览器差异化; - 自动生成 manifest让多浏览器构建(Chrome、Firefox、Safari 等)无需手工维护清单字段,配合
include/exclude还能精准裁剪每个目标平台的入口点集合。
掌握了这份指南,你就能从"写文件"开始,完整地定义扩展的每个功能面——后台、弹窗、选项页、内容脚本、覆盖页与各类辅助脚本——剩下的打包与清单生成全部交给 WXT 完成。继续深入可以阅读 project-structure 了解目录全貌,或通过 entrypoint-loaders 理解入口点的加载时序。
- 前端
- 开发工具
- 构建工具
- 插件系统
【免费下载链接】wxt
⚡ Next-gen Web Extension Framework
相关推荐
Renovate 的 Hermit 管理器:私包凭证、Git 凭据透传与嵌套环境配置实战指南
Renovate 的 Hermit 管理器:私包凭证、Git 凭据透传与嵌套环境配置实战指南 导读 本文聚焦 Renovate 中用于管理 Hermit htt
前端开发工具构建工具插件系统如何为vanilla-extract项目自动化生成TypeScript类型文档:完整指南
如何为vanilla extract项目自动化生成TypeScript类型文档:完整指南 vanilla extract是一个强大的零运行时TypeScript
前端开发工具vanilla-extract的TypeScript类型生成:自动化类型定义
vanilla extract的TypeScript类型生成:自动化类型定义 你是否还在为CSS样式与TypeScript类型不同步而烦恼?手动编写样式类型定义
前端开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考