extension-toolchain

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Extension Toolchain

浏览器扩展工具链

Modern browser extension development with Manifest V3, focusing on framework-agnostic solutions.
基于Manifest V3的现代浏览器扩展开发,聚焦于与框架无关的解决方案。

Recommended Stack: WXT (Default)

推荐技术栈:WXT(默认选项)

Why WXT (2025):
  • Framework-agnostic (React, Vue, Svelte, SolidJS, Vanilla)
  • Vite-powered (fast HMR, optimized builds)
  • Auto-reload on code changes (content scripts too!)
  • TypeScript-first with excellent type generation
  • Automated publishing to stores
  • Manifest V3 by default
bash
undefined
为什么选择WXT(2025年):
  • 与框架无关(支持React、Vue、Svelte、SolidJS、原生JS)
  • 基于Vite构建(快速HMR、优化的构建流程)
  • 代码变更时自动重载(包括内容脚本!)
  • 优先支持TypeScript,具备出色的类型生成能力
  • 支持自动发布至应用商店
  • 默认采用Manifest V3
bash
undefined

Create new extension

创建新扩展

npm create wxt@latest
npm create wxt@latest

Choose your framework

选择你的框架

? Select a template:
vanilla react vue svelte solid
? Select a template:
vanilla react vue svelte solid

Start development

启动开发服务

cd my-extension npm run dev # Chrome (default) npm run dev:firefox # Firefox npm run dev:edge # Edge npm run dev:safari # Safari (experimental)
undefined
cd my-extension npm run dev # Chrome(默认) npm run dev:firefox # Firefox npm run dev:edge # Edge npm run dev:safari # Safari(实验性支持)
undefined

When to Use WXT

何时使用WXT

✅ Multi-framework teams (framework-agnostic) ✅ Need cross-browser compatibility ✅ Want modern DX (HMR, TypeScript, auto-reload) ✅ Publishing to multiple stores ✅ Complex extensions with multiple entry points
✅ 多框架团队(与框架无关) ✅ 需要跨浏览器兼容性 ✅ 追求现代化开发体验(HMR、TypeScript、自动重载) ✅ 发布至多个应用商店 ✅ 包含多个入口点的复杂扩展

Alternative: Plasmo

替代方案:Plasmo

Best for React developers:
  • Next.js-like file-based routing
  • Automatic code splitting
  • Built-in remote code bundling
  • Very opinionated (React-centric)
bash
undefined
最适合React开发者:
  • 类Next.js的基于文件的路由机制
  • 自动代码分割
  • 内置远程代码打包功能
  • 高度约定式(以React为中心)
bash
undefined

Create Plasmo extension

创建Plasmo扩展

npm create plasmo
npm create plasmo

Start development

启动开发服务

npm run dev
undefined
npm run dev
undefined

When to Use Plasmo

何时使用Plasmo

✅ React-only team ✅ Want Next.js-like DX ✅ Need remote code bundling ✅ Prefer opinionated frameworks
✅ 纯React技术栈团队 ✅ 追求类Next.js的开发体验 ✅ 需要远程代码打包功能 ✅ 偏好约定式框架

Alternative: CRXJS (Vite Plugin)

替代方案:CRXJS(Vite插件)

Minimal, unopinionated:
  • Just a Vite plugin (you control everything)
  • Best-in-class HMR (especially for content scripts)
  • Lightweight, minimal overhead
  • Requires more manual setup
bash
undefined
轻量、无约定:
  • 仅作为Vite插件(完全由你掌控所有配置)
  • 业界领先的HMR(尤其针对内容脚本)
  • 轻量级,开销极小
  • 需要更多手动配置
bash
undefined

Add to existing Vite project

添加至现有Vite项目

npm install @crxjs/vite-plugin -D
undefined
npm install @crxjs/vite-plugin -D
undefined

When to Use CRXJS

何时使用CRXJS

✅ Want maximum control ✅ Already using Vite ✅ Minimal tooling preference ✅ Expert developer team
✅ 希望获得最大控制权 ✅ 已在使用Vite ✅ 偏好极简工具链 ✅ 资深开发团队

Toolchain Comparison

工具链对比

WXTPlasmoCRXJS
FrameworksAllReact-focusedAll
SetupBatteries-includedOpinionatedManual
DXExcellentExcellentGreat
HMRYesYesBest
Auto-publishYesYesNo
Learning CurveLowLowMedium
FlexibilityHighMediumHighest
WXTPlasmoCRXJS
支持框架全部聚焦React全部
配置难度开箱即用约定式手动配置
开发体验优秀优秀良好
HMR支持最佳
自动发布
学习曲线中等
灵活性中等最高

Project Structure (WXT)

项目结构(WXT)

my-extension/
├── entrypoints/
│   ├── background.ts      # Service worker
│   ├── content.ts         # Content script
│   ├── popup/             # Extension popup
│   │   ├── index.html
│   │   └── main.tsx
│   └── options/           # Options page
│       ├── index.html
│       └── main.tsx
├── components/            # Shared UI components
├── utils/                 # Shared utilities
├── public/                # Static assets
│   └── icon.png          # Extension icon
├── wxt.config.ts         # WXT configuration
└── package.json
my-extension/
├── entrypoints/
│   ├── background.ts      # Service Worker
│   ├── content.ts         # 内容脚本
│   ├── popup/             # 扩展弹窗
│   │   ├── index.html
│   │   └── main.tsx
│   └── options/           # 选项页面
│       ├── index.html
│       └── main.tsx
├── components/            # 共享UI组件
├── utils/                 # 共享工具函数
├── public/                # 静态资源
│   └── icon.png          # 扩展图标
├── wxt.config.ts         # WXT配置文件
└── package.json

Manifest V3 Essentials

Manifest V3核心配置

typescript
// wxt.config.ts
import { defineConfig } from 'wxt'

export default defineConfig({
  manifest: {
    name: 'My Extension',
    version: '1.0.0',
    permissions: ['storage', 'tabs'],
    host_permissions: ['https://*.example.com/*'],
    action: {
      default_title: 'My Extension',
    },
  },
})
typescript
// wxt.config.ts
import { defineConfig } from 'wxt'

export default defineConfig({
  manifest: {
    name: 'My Extension',
    version: '1.0.0',
    permissions: ['storage', 'tabs'],
    host_permissions: ['https://*.example.com/*'],
    action: {
      default_title: 'My Extension',
    },
  },
})

Key Manifest V3 Changes

Manifest V3关键变更

  • Service Workers replace background pages (no DOM access)
  • host_permissions
    separate from
    permissions
  • scripting
    API
    for dynamic content script injection
  • No remotely hosted code (bundle everything)
  • Limited
    executeScript
    capabilities
  • Service Worker 替代后台页面(无DOM访问权限)
  • host_permissions
    permissions
    分离
  • scripting
    API
    用于动态注入内容脚本
  • 禁止远程托管代码(需打包所有代码)
  • executeScript
    功能受限

Communication Patterns

通信模式

Popup ↔ Background

弹窗 ↔ 后台

typescript
// popup/main.tsx
import browser from 'webextension-polyfill'

const response = await browser.runtime.sendMessage({
  type: 'GET_DATA',
  payload: { key: 'value' },
})

// background.ts
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.type === 'GET_DATA') {
    // Process and respond
    sendResponse({ data: 'result' })
  }
  return true // Keep channel open for async response
})
typescript
// popup/main.tsx
import browser from 'webextension-polyfill'

const response = await browser.runtime.sendMessage({
  type: 'GET_DATA',
  payload: { key: 'value' },
})

// background.ts
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.type === 'GET_DATA') {
    // 处理并返回响应
    sendResponse({ data: 'result' })
  }
  return true // 保持通道开放以支持异步响应
})

Content Script ↔ Background

内容脚本 ↔ 后台

typescript
// content.ts
import browser from 'webextension-polyfill'

// Send message to background
const result = await browser.runtime.sendMessage({
  type: 'ANALYZE_PAGE',
  url: window.location.href,
})

// background.ts
browser.runtime.onMessage.addListener(async (message) => {
  if (message.type === 'ANALYZE_PAGE') {
    const analysis = await analyzePage(message.url)
    return { analysis }
  }
})
typescript
// content.ts
import browser from 'webextension-polyfill'

// 向后台发送消息
const result = await browser.runtime.sendMessage({
  type: 'ANALYZE_PAGE',
  url: window.location.href,
})

// background.ts
browser.runtime.onMessage.addListener(async (message) => {
  if (message.type === 'ANALYZE_PAGE') {
    const analysis = await analyzePage(message.url)
    return { analysis }
  }
})

Content Script ↔ Page (Web Page)

内容脚本 ↔ 网页

typescript
// content.ts - inject into page context
const script = document.createElement('script')
script.src = browser.runtime.getURL('injected.js')
document.head.appendChild(script)

// Listen for messages from page
window.addEventListener('message', (event) => {
  if (event.source !== window) return
  if (event.data.type === 'FROM_PAGE') {
    // Handle message from page
  }
})

// injected.js (runs in page context, has access to page's window/DOM)
window.postMessage({ type: 'FROM_PAGE', data: 'value' }, '*')
typescript
// content.ts - 注入到页面上下文
const script = document.createElement('script')
script.src = browser.runtime.getURL('injected.js')
document.head.appendChild(script)

// 监听来自网页的消息
window.addEventListener('message', (event) => {
  if (event.source !== window) return
  if (event.data.type === 'FROM_PAGE') {
    // 处理来自网页的消息
  }
})

// injected.js(运行在页面上下文,可访问页面的window/DOM)
window.postMessage({ type: 'FROM_PAGE', data: 'value' }, '*')

Storage Patterns

存储模式

typescript
// Using chrome.storage.sync (syncs across devices)
import browser from 'webextension-polyfill'

// Save
await browser.storage.sync.set({ key: 'value' })

// Load
const { key } = await browser.storage.sync.get('key')

// Listen for changes
browser.storage.onChanged.addListener((changes, areaName) => {
  if (areaName === 'sync' && changes.key) {
    console.log('Value changed:', changes.key.newValue)
  }
})
typescript
// 使用chrome.storage.sync(跨设备同步)
import browser from 'webextension-polyfill'

// 保存数据
await browser.storage.sync.set({ key: 'value' })

// 加载数据
const { key } = await browser.storage.sync.get('key')

// 监听数据变更
browser.storage.onChanged.addListener((changes, areaName) => {
  if (areaName === 'sync' && changes.key) {
    console.log('值已变更:', changes.key.newValue)
  }
})

Essential Libraries

核心依赖库

bash
undefined
bash
undefined

Cross-browser compatibility

跨浏览器兼容性

npm install webextension-polyfill
npm install webextension-polyfill

State Management

状态管理

npm install zustand
npm install zustand

Forms

表单处理

npm install react-hook-form zod
npm install react-hook-form zod

UI Components (if using React)

UI组件(若使用React)

npm install @radix-ui/react-* # Headless components
undefined
npm install @radix-ui/react-* # 无头组件
undefined

Testing Strategy

测试策略

bash
undefined
bash
undefined

Install testing libraries

安装测试依赖

npm install --save-dev vitest @testing-library/react @testing-library/user-event npm install --save-dev @wxt-dev/testing

**Example test:**
```typescript
// popup/main.test.tsx
import { render, screen } from '@testing-library/react'
import { describe, it, expect } from 'vitest'
import Popup from './main'

describe('Popup', () => {
  it('renders heading', () => {
    render(<Popup />)
    expect(screen.getByRole('heading')).toBeInTheDocument()
  })
})
npm install --save-dev vitest @testing-library/react @testing-library/user-event npm install --save-dev @wxt-dev/testing

**测试示例:**
```typescript
// popup/main.test.tsx
import { render, screen } from '@testing-library/react'
import { describe, it, expect } from 'vitest'
import Popup from './main'

describe('Popup', () => {
  it('渲染标题', () => {
    render(<Popup />)
    expect(screen.getByRole('heading')).toBeInTheDocument()
  })
})

Quality Gates Integration

质量门禁集成

yaml
undefined
yaml
undefined

.github/workflows/extension-ci.yml

.github/workflows/extension-ci.yml

name: Extension CI
on: [pull_request]
jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 - run: npm ci - run: npm run lint - run: npm run typecheck - run: npm test - run: npm run build
  - name: Upload build artifact
    uses: actions/upload-artifact@v4
    with:
      name: extension-build
      path: .output/
undefined
name: Extension CI
on: [pull_request]
jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 - run: npm ci - run: npm run lint - run: npm run typecheck - run: npm test - run: npm run build
  - name: 上传构建产物
    uses: actions/upload-artifact@v4
    with:
      name: extension-build
      path: .output/
undefined

Publishing Automation

发布自动化

bash
undefined
bash
undefined

Build for all browsers

为所有浏览器构建

npm run build # Chrome npm run build:firefox # Firefox npm run build:safari # Safari
npm run build # Chrome npm run build:firefox # Firefox npm run build:safari # Safari

Zip for submission

打包用于提交

npm run zip # All stores
npm run zip # 所有商店通用

Or use WXT's publish command (requires API keys)

或使用WXT的发布命令(需要API密钥)

wxt publish --chrome --firefox

**Store submission setup:**
```typescript
// wxt.config.ts
export default defineConfig({
  zip: {
    artifactTemplate: '{{name}}-{{version}}-{{browser}}.zip',
  },
  manifest: {
    name: '__MSG_extName__',
    description: '__MSG_extDescription__',
    default_locale: 'en',
  },
})
wxt publish --chrome --firefox

**商店提交配置:**
```typescript
// wxt.config.ts
export default defineConfig({
  zip: {
    artifactTemplate: '{{name}}-{{version}}-{{browser}}.zip',
  },
  manifest: {
    name: '__MSG_extName__',
    description: '__MSG_extDescription__',
    default_locale: 'en',
  },
})

Performance Best Practices

性能最佳实践

  • Lazy load content scripts: Only inject when needed
  • Use storage efficiently: Minimize sync storage writes
  • Debounce frequent operations: Especially in content scripts
  • Minimize background script work: Use alarms/events, not intervals
  • Optimize bundle size: Code splitting, tree shaking
  • 懒加载内容脚本:仅在需要时注入
  • 高效使用存储:减少同步存储的写入操作
  • 防抖频繁操作:尤其在内容脚本中
  • 最小化后台脚本工作量:使用定时器/事件,而非间隔轮询
  • 优化包体积:代码分割、摇树优化

Security Considerations

安全注意事项

typescript
// Content Security Policy
manifest: {
  content_security_policy: {
    extension_pages: "script-src 'self'; object-src 'self'"
  }
}

// Validate messages
browser.runtime.onMessage.addListener((message) => {
  // Always validate message structure
  if (typeof message !== 'object' || !message.type) {
    return
  }

  // Type guard
  if (message.type === 'EXPECTED_TYPE') {
    // Process
  }
})

// Never inject user content directly into DOM
// Use textContent, not innerHTML
element.textContent = userInput // Safe
element.innerHTML = userInput   // XSS vulnerability!
typescript
// 内容安全策略
manifest: {
  content_security_policy: {
    extension_pages: "script-src 'self'; object-src 'self'"
  }
}

// 验证消息合法性
browser.runtime.onMessage.addListener((message) => {
  // 始终验证消息结构
  if (typeof message !== 'object' || !message.type) {
    return
  }

  // 类型守卫
  if (message.type === 'EXPECTED_TYPE') {
    // 处理逻辑
  }
})

// 切勿直接将用户内容注入DOM
// 使用textContent,而非innerHTML
element.textContent = userInput // 安全
element.innerHTML = userInput   // 存在XSS漏洞!

Common Gotchas

常见陷阱

Service Worker Lifecycle:
  • Service workers can be terminated anytime
  • Use
    chrome.storage
    for persistence, not in-memory state
  • Set up event listeners at top level (not inside async functions)
Content Script Isolation:
  • Content scripts run in isolated world
  • No direct access to page's JavaScript
  • Must use
    postMessage
    to communicate with page
Manifest V3 Restrictions:
  • No
    eval()
    or
    new Function()
  • No inline scripts in HTML
  • All code must be bundled
  • Limited service worker APIs
Service Worker生命周期:
  • Service Worker可能随时被终止
  • 使用
    chrome.storage
    进行持久化存储,而非内存状态
  • 在顶层注册事件监听器(不要放在异步函数内)
内容脚本隔离:
  • 内容脚本运行在隔离环境
  • 无法直接访问网页的JavaScript
  • 必须使用
    postMessage
    与网页通信
Manifest V3限制:
  • 禁止使用
    eval()
    new Function()
  • HTML中禁止内联脚本
  • 所有代码必须打包
  • Service Worker API受限

Recommendation Flow

选型推荐流程

New browser extension:
├─ Multi-framework team → WXT ✅
├─ React-only team → Plasmo
└─ Want maximum control → CRXJS

Existing extension (Manifest V2):
└─ Migrate to WXT (handles V2→V3 migration)
When agents design browser extensions, they should:
  • Default to WXT for new projects (framework-agnostic, best DX)
  • Use Manifest V3 (V2 deprecated in 2024)
  • Apply quality-gates skill for testing/CI setup
  • Use webextension-polyfill for cross-browser compatibility
  • Follow Content Security Policy strictly
  • Plan for service worker lifecycle (no persistent background page)
  • Use chrome.storage for state persistence
  • Validate all messages between components
新建浏览器扩展:
├─ 多框架团队 → WXT ✅
├─ 纯React团队 → Plasmo
└─ 希望获得最大控制权 → CRXJS

现有扩展(Manifest V2):
└─ 迁移至WXT(支持V2→V3迁移)
当开发人员设计浏览器扩展时,应遵循以下原则:
  • 新项目默认选择WXT(与框架无关,开发体验最佳)
  • 使用Manifest V3(V2已于2024年废弃)
  • 应用质量门禁能力搭建测试/CI流程
  • 使用webextension-polyfill保证跨浏览器兼容性
  • 严格遵循内容安全策略
  • 考虑Service Worker生命周期(无持久后台页面)
  • 使用chrome.storage进行状态持久化
  • 验证所有组件间的通信消息