Loading...
Loading...
Compare original and translation side by side
undefinedundefinedvanilla react vue svelte solid
vanilla react vue svelte solid
undefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedundefinedundefined| WXT | Plasmo | CRXJS | |
|---|---|---|---|
| Frameworks | All | React-focused | All |
| Setup | Batteries-included | Opinionated | Manual |
| DX | Excellent | Excellent | Great |
| HMR | Yes | Yes | Best |
| Auto-publish | Yes | Yes | No |
| Learning Curve | Low | Low | Medium |
| Flexibility | High | Medium | Highest |
| WXT | Plasmo | CRXJS | |
|---|---|---|---|
| 支持框架 | 全部 | 聚焦React | 全部 |
| 配置难度 | 开箱即用 | 约定式 | 手动配置 |
| 开发体验 | 优秀 | 优秀 | 良好 |
| HMR支持 | 是 | 是 | 最佳 |
| 自动发布 | 是 | 是 | 否 |
| 学习曲线 | 低 | 低 | 中等 |
| 灵活性 | 高 | 中等 | 最高 |
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.jsonmy-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// 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',
},
},
})// 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',
},
},
})host_permissionspermissionsscriptingexecuteScripthost_permissionspermissionsscriptingexecuteScript// 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
})// 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.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 }
}
})// 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.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' }, '*')// 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' }, '*')// 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)
}
})// 使用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)
}
})undefinedundefinedundefinedundefinedundefinedundefined
**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()
})
})
**测试示例:**
```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()
})
})undefinedundefined - name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: extension-build
path: .output/undefined - name: 上传构建产物
uses: actions/upload-artifact@v4
with:
name: extension-build
path: .output/undefinedundefinedundefined
**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',
},
})
**商店提交配置:**
```typescript
// wxt.config.ts
export default defineConfig({
zip: {
artifactTemplate: '{{name}}-{{version}}-{{browser}}.zip',
},
manifest: {
name: '__MSG_extName__',
description: '__MSG_extDescription__',
default_locale: 'en',
},
})// 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!// 内容安全策略
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漏洞!chrome.storagepostMessageeval()new Function()chrome.storagepostMessageeval()new Function()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)新建浏览器扩展:
├─ 多框架团队 → WXT ✅
├─ 纯React团队 → Plasmo
└─ 希望获得最大控制权 → CRXJS
现有扩展(Manifest V2):
└─ 迁移至WXT(支持V2→V3迁移)