crxjs
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseCRXJS
CRXJS
CRXJS is a Chrome extension development tool that provides true HMR for popup, options, content scripts, and side panels. It reads your manifest to auto-generate the extension output, handles content script injection, and manages the service worker build. Under the hood it is a Vite plugin ().
@crxjs/vite-pluginCRXJS是一款Chrome扩展开发工具,可为弹窗、选项页、内容脚本和侧边栏提供真正的HMR。它读取你的清单文件自动生成扩展输出,处理内容脚本注入,并管理服务工作线程的构建。本质上它是一个Vite插件()。
@crxjs/vite-pluginCurrent status
当前状态
- Package: (v2.x stable, latest v2.4.0 as of March 2026)
@crxjs/vite-plugin - Scaffolding: (always use
npm create crxjs@latest)@latest - Maintained by: @Toumash and @FliPPeDround (since mid-2025)
- GitHub: github.com/crxjs/chrome-extension-tools (~4k stars)
- Vite compatibility: v3 through v8-beta
- 包: (v2.x稳定版,截至2026年3月最新版本为v2.4.0)
@crxjs/vite-plugin - 脚手架: (始终使用
npm create crxjs@latest)@latest - 维护者: @Toumash 和 @FliPPeDround(自2025年年中起)
- GitHub: github.com/crxjs/chrome-extension-tools(约4k星)
- Vite兼容性: v3至v8-beta
Quick start
快速开始
bash
undefinedbash
undefinedScaffold new project (picks framework interactively)
搭建新项目(交互式选择框架)
npm create crxjs@latest
npm create crxjs@latest
Or add to existing Vite project
或添加到现有Vite项目
npm install @crxjs/vite-plugin -D
undefinednpm install @crxjs/vite-plugin -D
undefinedVite config by framework
各框架的Vite配置
CRXJS is added as a Vite plugin. The setup varies slightly per framework.
CRXJS作为Vite插件添加,不同框架的设置略有差异。
React
React
typescript
// 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 })],
});Use (not ) for best HMR compatibility. If you must use SWC, cast the manifest:
@vitejs/plugin-reactplugin-react-swctypescript
import { ManifestV3Export } from "@crxjs/vite-plugin";
const manifest = manifestJson as ManifestV3Export;typescript
// 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兼容性,请使用(而非)。如果必须使用SWC,请对清单进行类型转换:
@vitejs/plugin-reactplugin-react-swctypescript
import { ManifestV3Export } from "@crxjs/vite-plugin";
const manifest = manifestJson as ManifestV3Export;Vue
Vue
typescript
import vue from "@vitejs/plugin-vue";
import { crx } from "@crxjs/vite-plugin";
import manifest from "./manifest.json";
export default defineConfig({
plugins: [vue(), crx({ manifest })],
});typescript
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
Svelte
typescript
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 })],
});typescript
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
原生TypeScript
typescript
import { crx } from "@crxjs/vite-plugin";
import manifest from "./manifest.json";
export default defineConfig({
plugins: [crx({ manifest })],
});typescript
import { crx } from "@crxjs/vite-plugin";
import manifest from "./manifest.json";
export default defineConfig({
plugins: [crx({ manifest })],
});defineManifest — type-safe dynamic manifest
defineManifest — 类型安全的动态清单
Instead of a static JSON file, use CRXJS's for dynamic values and full TypeScript autocompletion:
defineManifesttypescript
// 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",
},
}));Import in vite.config.ts:
typescript
import manifest from "./manifest";
// ... crx({ manifest })无需使用静态JSON文件,可使用CRXJS的实现动态值和完整的TypeScript自动补全:
defineManifesttypescript
// 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中导入:
typescript
import manifest from "./manifest";
// ... crx({ manifest })Type declarations
类型声明
Add to a or :
src/vite-env.d.tssrc/crxjs.d.tstypescript
/// <reference types="@crxjs/vite-plugin/client" />This enables types for and imports.
?script?script&module添加到或:
src/vite-env.d.tssrc/crxjs.d.tstypescript
/// <reference types="@crxjs/vite-plugin/client" />这将启用和导入的类型支持。
?script?script&moduleHMR behavior by context
不同上下文的HMR行为
| Context | HMR | How it works |
|---|---|---|
| Popup | Full HMR | WebSocket-based, state preserved |
| Options page | Full HMR | Same as popup |
| Side panel | Full HMR | Same as popup |
| Content script (manifest) | True HMR | CRXJS injects loader + HMR client |
| Content script (dynamic) | True HMR | Via |
| Service worker | Auto-reload | Changes trigger full extension reload |
| Main world scripts | No HMR | Skipped by CRXJS loader |
Content script HMR works because CRXJS generates a loader script that imports an HMR preamble, the HMR client, and your actual script — enabling real module-level HMR without full page reload. This is CRXJS's main differentiator.
| 上下文 | HMR支持 | 工作原理 |
|---|---|---|
| 弹窗 | 完整HMR | 基于WebSocket,保留状态 |
| 选项页 | 完整HMR | 与弹窗相同 |
| 侧边栏 | 完整HMR | 与弹窗相同 |
| 清单中配置的内容脚本 | 真正HMR | CRXJS注入加载器 + HMR客户端 |
| 动态内容脚本 | 真正HMR | 通过 |
| 服务工作线程 | 自动重载 | 变更触发扩展完整重载 |
| 主世界脚本 | 无HMR | 被CRXJS加载器跳过 |
内容脚本的HMR之所以可行,是因为CRXJS会生成一个加载器脚本,导入HMR预编译代码、HMR客户端以及你的实际脚本——无需整页重载即可实现真正的模块级HMR,这是CRXJS的核心优势。
Dynamic content script imports
动态内容脚本导入
For content scripts injected programmatically (not in manifest), CRXJS provides special import suffixes:
typescript
// 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],
});
});For main world injection (no HMR):
typescript
import mainWorldScript from "./inject?script&module";
await chrome.scripting.executeScript({
target: { tabId },
world: "MAIN",
files: [mainWorldScript],
});对于通过编程方式注入(而非在清单中配置)的内容脚本,CRXJS提供了特殊的导入后缀:
typescript
// background.ts — ?script会为executeScript提供解析后的路径
import contentScript from "./content?script";
chrome.action.onClicked.addListener(async (tab) => {
await chrome.scripting.executeScript({
target: { tabId: tab.id! },
files: [contentScript],
});
});对于主世界注入(无HMR):
typescript
import mainWorldScript from "./inject?script&module";
await chrome.scripting.executeScript({
target: { tabId },
world: "MAIN",
files: [mainWorldScript],
});CRXJS plugin options
CRXJS插件选项
typescript
crx({
manifest,
browser: "chrome", // 'chrome' | 'firefox'
contentScripts: {
injectCss: true, // auto-inject CSS for content scripts
hmrTimeout: 5000, // HMR connection timeout (ms)
},
});typescript
crx({
manifest,
browser: "chrome", // 'chrome' | 'firefox'
contentScripts: {
injectCss: true, // 自动为内容脚本注入CSS
hmrTimeout: 5000, // HMR连接超时时间(毫秒)
},
});Development workflow
开发流程
bash
undefinedbash
undefinedStart dev server (outputs to dist/ with HMR)
启动开发服务器(输出到dist/目录,支持HMR)
npm run dev
npm run dev
1. Open chrome://extensions
1. 打开chrome://extensions
2. Enable "Developer mode"
2. 启用「开发者模式」
3. Click "Load unpacked"
3. 点击「加载已解压的扩展程序」
4. Select the dist/ directory
4. 选择dist/目录
5. Edit code — popup/content scripts update instantly via HMR
5. 编辑代码——弹窗/内容脚本通过HMR即时更新
6. Service worker changes trigger automatic extension reload
6. 服务工作线程变更会触发扩展自动重载
After loading once, subsequent `npm run dev` sessions reconnect automatically. No need to re-load the extension unless manifest.json changes.
加载一次后,后续的`npm run dev`会话会自动重新连接。除非manifest.json变更,否则无需重新加载扩展。Production build
生产构建
bash
npm run build # outputs to dist/The dist/ directory is ready to zip and upload to Chrome Web Store:
bash
cd dist && zip -r ../extension.zip .Disable Vite's module preload to avoid CWS rejection of inline scripts:
typescript
build: {
modulePreload: false;
}bash
npm run build # 输出到dist/目录dist/目录可直接压缩后上传至Chrome网上应用店:
bash
cd dist && zip -r ../extension.zip .禁用Vite的模块预加载,避免内联脚本被Chrome网上应用店拒绝:
typescript
build: {
modulePreload: false;
}Known issues and workarounds
已知问题与解决方案
Tailwind CSS HMR in content scripts
内容脚本中的Tailwind CSS HMR问题
New Tailwind classes may not trigger CSS updates in content scripts. Workaround: restart dev server after adding new utility classes. Improved in v2.4.0 but not fully resolved. Ensure in config.
injectCss: true新增的Tailwind类可能无法触发内容脚本的CSS更新。解决方案:添加新工具类后重启开发服务器。v2.4.0版本已优化但未完全解决。确保配置中。
injectCss: trueWebSocket connection errors (ws://localhost:undefined/
)
ws://localhost:undefined/WebSocket连接错误(ws://localhost:undefined/
)
ws://localhost:undefined/Cause: port mismatch between dev server and HMR config. Fix: explicitly set both to the same value:
typescript
server: {
port: 5173,
strictPort: true,
hmr: { port: 5173 },
}原因:开发服务器端口与HMR配置不匹配。修复:显式将两者设置为相同值:
typescript
server: {
port: 5173,
strictPort: true,
hmr: { port: 5173 },
}"Manifest version 2 is deprecated" warning
「Manifest版本2已弃用」警告
If you see this, your manifest is being interpreted as MV2. Fix: ensure is set.
"manifest_version": 3如果看到此提示,说明你的清单被识别为MV2。修复:确保设置。
"manifest_version": 3Content scripts not injecting on file:// URLs
内容脚本无法在file:// URL上注入
Chrome requires the user to enable "Allow access to file URLs" in the extension settings at chrome://extensions. CRXJS cannot change this.
Chrome要求用户在chrome://extensions的扩展设置中启用「允许访问文件URL」。CRXJS无法修改此设置。
HMR stops working after Chrome update
Chrome更新后HMR停止工作
CRXJS's HMR relies on injecting a content script that connects to the dev server's WebSocket. Chrome security updates occasionally break this. Fix: update to the latest CRXJS version, which tracks Chrome changes.
CRXJS的HMR依赖注入连接到开发服务器WebSocket的内容脚本。Chrome安全更新偶尔会破坏此功能。修复:升级到最新版本的CRXJS,它会跟进Chrome的变更。
CRXJS vs alternatives
CRXJS与竞品对比
| Feature | CRXJS | WXT | Plasmo |
|---|---|---|---|
| Content script HMR | True HMR | File-based reload | Partial |
| Framework support | Any Vite framework | Any | React-focused |
| Abstraction level | Thin (Vite plugin) | Full framework | Full framework |
| Messaging helpers | None (use chrome.* directly) | Built-in | Built-in |
| Storage wrappers | None | Built-in | Built-in |
| Cross-browser | Chrome + Firefox | Chrome + Firefox + Safari | Chrome + Firefox |
| File-based routing | No | Yes | Yes |
| Learning curve | Low (know Vite, know CRXJS) | Medium | Medium |
Choose CRXJS when: you want minimal abstraction over raw Chrome APIs and value content script HMR above all. CRXJS stays out of the way — no magic routing, no wrapper APIs, just your code with HMR.
Choose WXT when: you want conventions, built-in utilities, and cross-browser support.
Choose Plasmo when: you're React-focused and want the highest-level abstraction.
| 特性 | CRXJS | WXT | Plasmo |
|---|---|---|---|
| 内容脚本HMR | 真正HMR | 文件级重载 | 部分支持 |
| 框架支持 | 任意Vite框架 | 任意框架 | 侧重React |
| 抽象层级 | 轻量(Vite插件) | 完整框架 | 完整框架 |
| 消息通信助手 | 无(直接使用chrome.* API) | 内置 | 内置 |
| 存储封装 | 无 | 内置 | 内置 |
| 跨浏览器支持 | Chrome + Firefox | Chrome + Firefox + Safari | Chrome + Firefox |
| 文件路由 | 无 | 有 | 有 |
| 学习曲线 | 低(懂Vite即可) | 中等 | 中等 |
选择CRXJS的场景:你希望尽量贴近原生Chrome API,且最看重内容脚本的HMR功能。CRXJS不会过度干预——无魔法路由、无封装API,仅为你的代码提供HMR支持。
选择WXT的场景:你需要约定式规范、内置工具和多浏览器支持。
选择Plasmo的场景:你专注于React开发,需要最高层级的抽象。
Project structure (recommended)
推荐项目结构
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.jsonCRXJS resolves HTML files referenced in the manifest automatically. Your popup.html can use standard and it works.
<script type="module" src="./main.tsx">If you encounter a bug or unexpected behavior in CRXJS, open an issue at github.com/crxjs/chrome-extension-tools/issues.
my-extension/
├── src/
│ ├── background/
│ │ └── index.ts
│ ├── content/
│ │ ├── index.ts
│ │ └── styles.css
│ ├── popup/
│ │ ├── index.html <- CRXJS会自动解析HTML入口
│ │ ├── App.tsx
│ │ └── main.tsx
│ ├── options/
│ │ ├── index.html
│ │ └── main.tsx
│ ├── sidepanel/
│ │ ├── index.html
│ │ └── main.tsx
│ └── shared/
│ ├── messages.ts
│ └── storage.ts
├── public/
│ └── icons/
├── manifest.ts <- 或manifest.json
├── vite.config.ts
├── tsconfig.json
└── package.jsonCRXJS会自动解析清单中引用的HTML文件。你的popup.html可以使用标准的,完全兼容。
<script type="module" src="./main.tsx">如果你在CRXJS中遇到bug或异常行为,请在github.com/crxjs/chrome-extension-tools/issues提交问题。