关于 crxjs
CRXJS 是一个依据扩展清单构建 Chrome 扩展的 Vite 插件。它为多种扩展界面提供 HMR,也支持内容脚本的真正 HMR,同时保持对浏览器 API 的轻量封装。
主要功能
- 01基于清单构建扩展
- 02弹出窗口、选项页和侧边栏 HMR
- 03内容脚本 HMR
- 04动态内容脚本导入
- 05类型安全的动态清单
适用场景
- 01使用 Vite 框架开发扩展
- 02开发时迭代内容脚本
- 03构建待发布的扩展包
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ samber/ cc-skills --skill crxjs安装后在 Claude Code 中输入/crxjs确认已加载。
详细介绍
CRXJS
CRXJS 是一款 Chrome 扩展开发工具,可为弹出窗口、选项页、内容脚本和侧边栏提供真正的 HMR。它会读取 manifest,自动生成扩展输出,处理内容脚本注入,并管理 service worker 构建。其底层是一个 Vite 插件(@crxjs/vite-plugin)。
当前状态
- 软件包:
@crxjs/vite-plugin(v2.x 稳定版,截至 2026 年 3 月的最新版本为 v2.4.0) - 脚手架:
npm create crxjs@latest(始终使用@latest) - 维护者:@Toumash 和 @FliPPeDround(自 2025 年年中起)
- GitHub:github.com/crxjs/chrome-extension-tools(约 4k 星标)
- Vite 兼容性:v3 至 v8-beta
快速开始
# Scaffold new project (picks framework interactively)
npm create crxjs@latest
# Or add to existing Vite project
npm install @crxjs/vite-plugin -D
各框架的 Vite 配置
CRXJS 作为 Vite 插件添加。不同框架的配置略有差异。
React
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { crx } from "@crxjs/vite-plugin";
import manifest from "./manifest.json";
export default defineConfig({
plugins: [react(), crx({ manifest })],
});
为获得最佳 HMR 兼容性,请使用 @vitejs/plugin-react(而不是 plugin-react-swc)。如果必须使用 SWC,请为 manifest 进行类型转换:
import { ManifestV3Export } from "@crxjs/vite-plugin";
const manifest = manifestJson as ManifestV3Export;
Vue
import vue from "@vitejs/plugin-vue";
import { crx } from "@crxjs/vite-plugin";
import manifest from "./manifest.json";
export default defineConfig({
plugins: [vue(), crx({ manifest })],
});
Svelte
import { svelte } from "@sveltejs/vite-plugin-svelte";
import { crx } from "@crxjs/vite-plugin";
import manifest from "./manifest.json";
export default defineConfig({
plugins: [svelte(), crx({ manifest })],
});
Vanilla TypeScript
import { crx } from "@crxjs/vite-plugin";
import manifest from "./manifest.json";
export default defineConfig({
plugins: [crx({ manifest })],
});
defineManifest — 类型安全的动态 manifest
你可以使用 CRXJS 的 defineManifest 代替静态 JSON 文件,以支持动态值和完整的 TypeScript 自动补全:
// manifest.ts
import { defineManifest } from "@crxjs/vite-plugin";
import pkg from "./package.json";
export default defineManifest((config) => ({
manifest_version: 3,
name: config.command === "serve" ? `[DEV] ${pkg.name}` : pkg.name,
version: pkg.version,
description: pkg.description,
permissions: ["storage", "activeTab", "scripting"],
action: {
default_popup: "src/popup/index.html",
default_icon: {
"16": "public/icons/icon16.png",
"48": "public/icons/icon48.png",
},
},
background: {
service_worker: "src/background/index.ts",
type: "module",
},
content_scripts: [
{
matches: ["https://*/*"],
js: ["src/content/index.ts"],
css: ["src/content/styles.css"],
},
],
options_page: "src/options/index.html",
side_panel: { default_path: "src/sidepanel/index.html" },
icons: {
"16": "public/icons/icon16.png",
"48": "public/icons/icon48.png",
"128": "public/icons/icon128.png",
},
}));
在 vite.config.ts 中导入:
import manifest from "./manifest";
// ... crx({ manifest })
类型声明
添加到 src/vite-env.d.ts 或 src/crxjs.d.ts:
/// <reference types="@crxjs/vite-plugin/client" />
这会为 ?script 和 ?script&module 导入启用类型支持。
不同上下文中的 HMR 行为
| 上下文 | HMR | 工作方式 |
|---|---|---|
| 弹出窗口 | 完整 HMR | 基于 WebSocket,保留状态 |
| 选项页 | 完整 HMR | 与弹出窗口相同 |
| 侧边栏 | 完整 HMR | 与弹出窗口相同 |
| 内容脚本(manifest) | 真正的 HMR | CRXJS 注入加载器和 HMR 客户端 |
| 内容脚本(动态) | 真正的 HMR | 通过 ?script 导入 |
| Service worker | 自动重新加载 | 更改会触发整个扩展重新加载 |
| Main world 脚本 | 无 HMR | CRXJS 加载器会跳过 |
内容脚本 HMR 能够正常工作,是因为 CRXJS 会生成一个加载器脚本,用于导入 HMR 前置代码、HMR 客户端和实际脚本,从而支持真正的模块级 HMR,而无需完整重新加载页面。这是 CRXJS 的主要差异化优势。
动态导入内容脚本
对于以编程方式注入(而非在 manifest 中声明)的内容脚本,CRXJS 提供了特殊的导入后缀:
// background.ts — ?script gives you a resolved path for executeScript
import contentScript from "./content?script";
chrome.action.onClicked.addListener(async (tab) => {
await chrome.scripting.executeScript({
target: { tabId: tab.id! },
files: [contentScript],
});
});
对于 Main world 注入(无 HMR):
import mainWorldScript from "./inject?script&module";
await chrome.scripting.executeScript({
target: { tabId },
world: "MAIN",
files: [mainWorldScript],
});
CRXJS 插件选项
crx({
manifest,
browser: "chrome", // 'chrome' | 'firefox'
contentScripts: {
injectCss: true, // auto-inject CSS for content scripts
hmrTimeout: 5000, // HMR connection timeout (ms)
},
});
开发工作流
# Start dev server (outputs to dist/ with HMR)
npm run dev
# 1. Open chrome://extensions
# 2. Enable "Developer mode"
# 3. Click "Load unpacked"
# 4. Select the dist/ directory
# 5. Edit code — popup/content scripts update instantly via HMR
# 6. Service worker changes trigger automatic extension reload
加载一次后,后续运行 npm run dev 时会自动重新连接。除非 manifest.json 发生更改,否则无需重新加载扩展。
生产构建
npm run build # outputs to dist/
dist/ 目录已准备就绪,可以压缩并上传到 Chrome Web Store:
cd dist && zip -r ../extension.zip .
禁用 Vite 的模块预加载,以避免 Chrome Web Store 因内联脚本而拒绝扩展:
build: {
modulePreload: false;
}
已知问题及解决方法
内容脚本中的 Tailwind CSS HMR
新增 Tailwind 类可能不会触发内容脚本中的 CSS 更新。解决方法:添加新的实用类后重启开发服务器。v2.4.0 已有所改进,但问题尚未完全解决。请确保在配置中设置 injectCss: true。
WebSocket 连接错误(ws://localhost:undefined/)
原因:开发服务器端口与 HMR 配置中的端口不一致。修复方法:显式将两者设置为相同的值:
server: {
port: 5173,
strictPort: true,
hmr: { port: 5173 },
}
“Manifest version 2 is deprecated” 警告
如果看到此警告,说明你的 manifest 被识别为 MV2。修复方法:确保设置了 "manifest_version": 3。
内容脚本未在 file:// URL 上注入
Chrome 要求用户在扩展设置中启用“允许访问文件网址”(chrome://extensions)。CRXJS 无法更改此设置。
Chrome 更新后 HMR 停止工作
CRXJS 的 HMR 依赖于注入一个连接到开发服务器 WebSocket 的内容脚本。Chrome 安全更新偶尔会导致此功能失效。修复方法:更新到最新版本的 CRXJS,该版本会跟进 Chrome 的变更。
CRXJS 与其他方案对比
| 特性 | CRXJS | WXT | Plasmo |
|---|---|---|---|
| 内容脚本 HMR | 真正的 HMR | 基于文件的重新加载 | 部分支持 |
| 框架支持 | 任意 Vite 框架 | 任意 | 专注于 React |
| 抽象层级 | 轻量(Vite 插件) | 完整框架 | 完整框架 |
| 消息传递辅助工具 | 无(直接使用 chrome.*) | 内置 | 内置 |
| 存储封装 | 无 | 内置 | 内置 |
| 跨浏览器支持 | Chrome + Firefox | Chrome + Firefox + Safari | Chrome + Firefox |
| 基于文件的路由 | 否 | 是 | 是 |
| 学习曲线 | 低(了解 Vite 和 CRXJS 即可) | 中 | 中 |
选择 CRXJS 的情况:你希望尽量减少对原始 Chrome API 的封装,并且最看重内容脚本 HMR。CRXJS 不会碍事——没有魔法路由,没有封装 API,只有带 HMR 的代码。
选择 WXT 的情况:你希望使用约定、内置实用工具和跨浏览器支持。
选择 Plasmo 的情况:你专注于 React,并希望获得最高层级的抽象。
项目结构(推荐)
my-extension/
├── src/
│ ├── background/
│ │ └── index.ts
│ ├── content/
│ │ ├── index.ts
│ │ └── styles.css
│ ├── popup/
│ │ ├── index.html <- CRXJS resolves HTML entry points
│ │ ├── App.tsx
│ │ └── main.tsx
│ ├── options/
│ │ ├── index.html
│ │ └── main.tsx
│ ├── sidepanel/
│ │ ├── index.html
│ │ └── main.tsx
│ └── shared/
│ ├── messages.ts
│ └── storage.ts
├── public/
│ └── icons/
├── manifest.ts <- or manifest.json
├── vite.config.ts
├── tsconfig.json
└── package.json
CRXJS 会自动解析清单中引用的 HTML 文件。你的 popup.html 可以使用标准的 <script type="module" src="./main.tsx">,并且能够正常运行。
如果你在 CRXJS 中遇到错误或意外行为,请在 github.com/crxjs/chrome-extension-tools/issues 提交问题。