本文基于 Manifest V3 标准,系统性地介绍 Chrome 浏览器扩展开发的核心知识,涵盖项目架构、核心组件、消息通信、存储方案、网络请求、安全实践、现代工具链集成等内容,并提供大量可运行的代码示例。
前言
Chrome 扩展是一种能够扩展浏览器功能的小型程序。它可以修改网页内容、添加新功能、与 Web 服务交互,甚至构建完整的应用程序。随着 Manifest V3 的全面推行,扩展开发迎来了重大变革:Background Pages 被 Service Workers 取代,网络请求拦截改用 declarativeNetRequest,安全策略更加严格。
| 特性 | Manifest V2 | Manifest V3 |
|---|---|---|
| 后台脚本 | Background Pages | Service Workers |
| 远程代码 | 允许 | 禁止 |
| eval() | 允许 | 禁止 |
| 网络请求拦截 | webRequest (blocking) | declarativeNetRequest |
| 内容安全策略 | 较宽松 | 更严格 |
| Host Permissions | 在 permissions 中 | 单独的 host_permissions |
| Promise 支持 | 部分支持 | 全面支持 |
本文将带你全面掌握现代 Chrome 扩展开发。
一、快速开始
1.1 最小可运行扩展
一个 Chrome 扩展至少需要一个 manifest.json 文件:
json
{
"manifest_version": 3,
"name": "我的第一个扩展",
"version": "1.0.0",
"description": "一个简单的 Chrome 扩展示例",
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
}
}
创建 popup.html:
html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body { width: 200px; padding: 16px; font-family: system-ui; }
h1 { font-size: 16px; margin: 0; }
</style>
</head>
<body>
<h1>👋 Hello Extension!</h1>
</body>
</html>
1.2 加载扩展
- 打开
chrome://extensions/ - 启用右上角的"开发者模式"
- 点击"加载已解压的扩展程序"
- 选择项目目录
二、项目结构
2.1 标准项目结构
bash
my-extension/
├── manifest.json # 扩展清单(必需)
├── background.js # Service Worker(后台脚本)
├── content-script.js # 内容脚本(注入网页)
├── popup.html/js/css # 弹出页面
├── sidebar.html/js/css # 侧边栏(Side Panel)
├── options.html/js/css # 设置页面
├── icons/ # 图标
│ ├── icon16.png
│ ├── icon48.png
│ └── icon128.png
├── _locales/ # 国际化
│ ├── en/messages.json
│ └── zh_CN/messages.json
└── lib/ # 第三方库
2.2 架构概览图
scss
┌───────────────────────────────────────────────────────────────────────────┐
│ 浏览器扩展架构图 │
├───────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ 消息通信 ┌─────────────────┐ │
│ │ Web Page │◄──────────────► │ Content Script │ │
│ │ (网页) │ postMessage │ (内容脚本) │ │
│ └─────────────────┘ └────────┬────────┘ │
│ │ │
│ chrome.runtime.sendMessage │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Background Service Worker │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ 消息路由 │ │ API 调用 │ │ 状态管理 │ │ 定时任务 │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Popup │ │ Side Panel │ │ Options │ │
│ │ (弹窗) │ │ (侧边栏) │ │ (设置页) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ 存储层 (Storage Layer) │ │
│ │ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ │
│ │ │ Local │ │ Sync │ │ Session │ │ IndexedDB │ │ │
│ │ │ Storage │ │ Storage │ │ Storage │ │ │ │ │
│ │ └────────────┘ └────────────┘ └────────────┘ └────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────────────────┘
2.3 各文件职责
| 文件 | 职责 | 运行环境 |
|---|---|---|
| manifest.json | 扩展配置和元数据 | - |
| background.js | 后台任务、事件监听、API调用 | Service Worker |
| content-script.js | 与网页交互、DOM操作 | 网页上下文 |
| sidebar.js/popup.js | 用户界面逻辑 | 扩展页面上下文 |
三、Manifest 配置详解
3.1 完整配置示例
json
{
"manifest_version": 3,
"name": "__MSG_extName__",
"version": "1.0.0",
"description": "__MSG_extDescription__",
"default_locale": "zh_CN",
"icons": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
},
"permissions": [
"storage",
"tabs",
"activeTab",
"scripting",
"sidePanel",
"contextMenus",
"alarms",
"notifications"
],
"optional_permissions": ["history", "bookmarks"],
"host_permissions": [
"https://*.example.com/*",
"https://api.openai.com/*"
],
"background": {
"service_worker": "background.js",
"type": "module"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content-script.js"],
"css": ["content-style.css"],
"run_at": "document_idle"
}
],
"side_panel": {
"default_path": "sidebar.html"
},
"action": {
"default_popup": "popup.html",
"default_icon": { "16": "icons/icon16.png", "48": "icons/icon48.png" },
"default_title": "点击打开"
},
"options_ui": {
"page": "options.html",
"open_in_tab": false
},
"commands": {
"toggle-sidebar": {
"suggested_key": { "default": "Ctrl+Shift+S", "mac": "Command+Shift+S" },
"description": "切换侧边栏"
}
},
"web_accessible_resources": [
{ "resources": ["images/*"], "matches": ["<all_urls>"] }
],
"externally_connectable": {
"matches": ["http://localhost:*/*", "https://*.yourdomain.com/*"]
}
}
3.2 关键字段说明
permissions vs host_permissions
json
// API 权限 - 访问 Chrome API
"permissions": [
"tabs", // 访问标签页信息
"storage", // 本地存储
"sidePanel", // 侧边栏功能
"activeTab", // 当前活动标签页
"scripting", // 动态注入脚本
"notifications", // 桌面通知
"contextMenus", // 右键菜单
"alarms" // 定时器
],
// 主机权限 - 访问指定网站
"host_permissions": [
"<all_urls>", // 所有网站
"https://*.google.com/*", // 特定域名
"http://localhost:*/*" // 本地开发
]
run_at 取值说明
document_start: DOM 开始构建时注入document_end: DOM 构建完成时注入(DOMContentLoaded 之前)document_idle: DOMContentLoaded 之后注入(默认,推荐)
3.3 常用权限速查表
| 权限 | 用途 |
|---|---|
storage |
本地存储 |
tabs |
标签页管理 |
activeTab |
临时访问当前标签页 |
scripting |
动态注入脚本 |
sidePanel |
侧边栏功能 |
contextMenus |
右键菜单 |
alarms |
定时器 |
notifications |
系统通知 |
cookies |
Cookie 管理 |
history |
浏览历史 |
bookmarks |
书签管理 |
downloads |
下载管理 |
offscreen |
离屏文档 |
declarativeNetRequest |
网络请求拦截 |
四、核心组件详解
4.1 Background Service Worker
Service Worker 是扩展的"大脑",负责事件监听、状态管理和跨组件通信。
javascript
// background.js
// ============ 生命周期事件 ============
chrome.runtime.onInstalled.addListener(async (details) => {
console.log('扩展已安装/更新:', details.reason)
if (details.reason === 'install') {
// 首次安装:初始化存储
await chrome.storage.local.set({
settings: { theme: 'light', enabled: true }
})
// 创建右键菜单
chrome.contextMenus.create({
id: 'main-menu',
title: '使用扩展处理',
contexts: ['selection', 'page']
})
}
})
// ============ 消息处理中心 ============
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
handleMessage(message, sender)
.then(sendResponse)
.catch(err => sendResponse({ error: err.message }))
return true // 异步响应必须返回 true
})
async function handleMessage(message, sender) {
switch (message.action) {
case 'getData':
return chrome.storage.local.get(message.key)
case 'setData':
await chrome.storage.local.set({ [message.key]: message.value })
return { success: true }
case 'openSidebar':
await chrome.sidePanel.open({ tabId: sender.tab.id })
return { success: true }
default:
throw new Error(`未知操作: ${message.action}`)
}
}
// ============ 定时任务 ============
chrome.alarms.create('keepAlive', { periodInMinutes: 0.5 })
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'keepAlive') {
console.log('Service Worker 保持活跃')
}
})
// ============ 快捷键 ============
chrome.commands.onCommand.addListener(async (command) => {
if (command === 'toggle-sidebar') {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true })
if (tab) await chrome.sidePanel.open({ tabId: tab.id })
}
})
// ============ 侧边栏配置 ============
chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true })
console.log('Background Service Worker 已启动')
4.2 Content Script
Content Script 注入到网页中运行,可以访问和操作 DOM。
javascript
// content-script.js
;(function() {
'use strict'
// 防止重复注入
if (window.__EXTENSION_LOADED__) return
window.__EXTENSION_LOADED__ = true
console.log('[扩展] Content Script 已注入:', location.href)
// 发送消息到 Background
async function sendMessage(action, data = {}) {
return new Promise((resolve, reject) => {
chrome.runtime.sendMessage({ action, ...data }, (response) => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError.message))
} else if (response?.error) {
reject(new Error(response.error))
} else {
resolve(response)
}
})
})
}
// 接收来自 Background 的消息
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
switch (message.action) {
case 'getPageInfo':
sendResponse({
url: location.href,
title: document.title,
content: document.body.innerText.slice(0, 5000)
})
break
case 'highlight':
highlightText(message.text)
sendResponse({ success: true })
break
}
return false
})
// 高亮文本
function highlightText(text) {
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT)
while (walker.nextNode()) {
const node = walker.currentNode
if (node.textContent.includes(text)) {
const mark = document.createElement('mark')
mark.style.cssText = 'background: yellow; padding: 2px 4px;'
mark.textContent = node.textContent
node.parentNode.replaceChild(mark, node)
}
}
}
// 与网页通信(可选)
window.addEventListener('message', async (event) => {
if (event.source !== window || event.data?.type !== 'FROM_PAGE') return
try {
const response = await sendMessage(event.data.action, event.data.payload)
window.postMessage({ type: 'FROM_EXTENSION', response }, '*')
} catch (error) {
window.postMessage({ type: 'FROM_EXTENSION', error: error.message }, '*')
}
})
})()
4.3 Side Panel(侧边栏)
html
<!-- sidebar.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: system-ui; background: #f5f5f5; }
.container { height: 100vh; display: flex; flex-direction: column; }
.header { padding: 16px; background: #4285f4; color: white; }
.content { flex: 1; padding: 16px; overflow-y: auto; }
.message { padding: 12px; margin: 8px 0; background: white; border-radius: 8px; }
.message.user { background: #e3f2fd; margin-left: 20%; }
.input-area { padding: 16px; border-top: 1px solid #ddd; background: white; }
textarea { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 8px; resize: none; }
button { margin-top: 8px; width: 100%; padding: 10px; background: #4285f4; color: white; border: none; border-radius: 8px; cursor: pointer; }
button:hover { background: #3367d6; }
button:disabled { background: #ccc; }
</style>
</head>
<body>
<div class="container">
<header class="header"><h1>🚀 我的扩展</h1></header>
<main class="content" id="messages"></main>
<div class="input-area">
<textarea id="input" rows="3" placeholder="输入内容... (Ctrl+Enter 发送)"></textarea>
<button id="send">发送</button>
</div>
</div>
<script src="sidebar.js"></script>
</body>
</html>
javascript
// sidebar.js
document.addEventListener('DOMContentLoaded', () => {
const input = document.getElementById('input')
const sendBtn = document.getElementById('send')
const messagesContainer = document.getElementById('messages')
function appendMessage(content, type = 'user') {
const div = document.createElement('div')
div.className = `message ${type}`
div.textContent = content
messagesContainer.appendChild(div)
messagesContainer.scrollTop = messagesContainer.scrollHeight
}
async function handleSend() {
const text = input.value.trim()
if (!text) return
appendMessage(text, 'user')
input.value = ''
sendBtn.disabled = true
try {
const response = await chrome.runtime.sendMessage({ action: 'process', text })
appendMessage(response.result || '处理完成', 'assistant')
} catch (error) {
appendMessage('错误: ' + error.message, 'error')
} finally {
sendBtn.disabled = false
}
}
sendBtn.addEventListener('click', handleSend)
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) handleSend()
})
})
五、消息通信机制
5.1 通信架构图
css
┌──────────┐ postMessage ┌──────────────┐ chrome.runtime ┌──────────────┐
│ Web Page │ ◄──────────────► │Content Script│ ◄────────────────► │ Background │
└──────────┘ └──────────────┘ └──────────────┘
▲ ▲
│ chrome.runtime.sendMessage │
▼ │
┌──────────────┐ │
│ Popup/Sidebar│ ◄────────────────────────┘
└──────────────┘ chrome.tabs.sendMessage
5.2 消息发送模式
javascript
// 1. Content Script / Popup / Sidebar → Background
chrome.runtime.sendMessage({ action: 'getData', key: 'settings' }, (response) => {
if (chrome.runtime.lastError) {
console.error('发送失败:', chrome.runtime.lastError.message)
return
}
console.log('响应:', response)
})
// 2. Background → Content Script(需要指定 tabId)
async function sendToActiveTab(message) {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true })
return chrome.tabs.sendMessage(tab.id, message)
}
// 3. 网页 ↔ Content Script(使用 postMessage)
// 网页端
window.postMessage({ type: 'FROM_PAGE', action: 'getData' }, '*')
// Content Script 端
window.addEventListener('message', (event) => {
if (event.source !== window || event.data.type !== 'FROM_PAGE') return
window.postMessage({ type: 'FROM_EXTENSION', data: {} }, '*')
})
5.3 长连接(Port)
javascript
// 建立连接
const port = chrome.runtime.connect({ name: 'sidebar' })
port.onMessage.addListener((message) => {
console.log('收到:', message)
})
port.postMessage({ type: 'subscribe', channel: 'updates' })
// Background 端监听
const connections = new Map()
chrome.runtime.onConnect.addListener((port) => {
connections.set(port.name, port)
port.onMessage.addListener((message) => {
port.postMessage({ type: 'ack', id: message.id })
})
port.onDisconnect.addListener(() => {
connections.delete(port.name)
})
})
六、数据存储
6.1 chrome.storage API
javascript
// 本地存储(无大小限制,不同步)
await chrome.storage.local.set({ key: 'value', settings: { theme: 'dark' } })
const { key, settings } = await chrome.storage.local.get(['key', 'settings'])
// 同步存储(跟随用户账号,限制 100KB)
await chrome.storage.sync.set({ preferences: { fontSize: 14 } })
// 会话存储(扩展关闭后清除)
await chrome.storage.session.set({ tempData: 'xxx' })
// 监听存储变化
chrome.storage.onChanged.addListener((changes, areaName) => {
for (const [key, { oldValue, newValue }] of Object.entries(changes)) {
console.log(`[${areaName}] ${key}: ${oldValue} → ${newValue}`)
}
})
6.2 封装存储工具类
javascript
class Storage {
constructor(area = 'local') {
this.storage = chrome.storage[area]
this.cache = new Map()
}
async get(key, defaultValue = null) {
if (this.cache.has(key)) return this.cache.get(key)
const result = await this.storage.get(key)
const value = result[key] ?? defaultValue
this.cache.set(key, value)
return value
}
async set(key, value) {
await this.storage.set({ [key]: value })
this.cache.set(key, value)
}
async remove(key) {
await this.storage.remove(key)
this.cache.delete(key)
}
}
const storage = new Storage('local')
await storage.set('user', { name: 'John' })
const user = await storage.get('user')
七、网络请求
7.1 HTTP 请求封装
javascript
class HttpClient {
constructor(baseURL = '', defaultHeaders = {}) {
this.baseURL = baseURL
this.defaultHeaders = { 'Content-Type': 'application/json', ...defaultHeaders }
}
async request(endpoint, options = {}) {
const response = await fetch(this.baseURL + endpoint, {
method: options.method || 'GET',
headers: { ...this.defaultHeaders, ...options.headers },
body: options.body ? JSON.stringify(options.body) : undefined
})
const data = await response.json()
if (!response.ok) throw new Error(data.message || `HTTP ${response.status}`)
return data
}
get(endpoint) { return this.request(endpoint) }
post(endpoint, body) { return this.request(endpoint, { method: 'POST', body }) }
}
const api = new HttpClient('https://api.example.com')
const data = await api.get('/users')
7.2 流式 AI 对话
javascript
class AIStreamClient {
constructor(apiKey, provider = 'openai') {
this.apiKey = apiKey
this.provider = provider
}
async chat(messages, onChunk, onComplete) {
const isAnthropic = this.provider === 'anthropic'
const url = isAnthropic
? 'https://api.anthropic.com/v1/messages'
: 'https://api.openai.com/v1/chat/completions'
const headers = {
'Content-Type': 'application/json',
...(isAnthropic
? { 'x-api-key': this.apiKey, 'anthropic-version': '2023-06-01' }
: { 'Authorization': `Bearer ${this.apiKey}` })
}
const response = await fetch(url, {
method: 'POST',
headers,
body: JSON.stringify({
model: isAnthropic ? 'claude-3-sonnet-20240229' : 'gpt-4',
messages,
stream: true,
max_tokens: 4096
})
})
const reader = response.body.getReader()
const decoder = new TextDecoder()
let fullContent = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
const lines = decoder.decode(value).split('\n')
for (const line of lines) {
if (!line.startsWith('data: ') || line.includes('[DONE]')) continue
try {
const json = JSON.parse(line.slice(6))
const content = isAnthropic
? json.delta?.text
: json.choices?.[0]?.delta?.content
if (content) {
fullContent += content
onChunk?.(content, fullContent)
}
} catch {}
}
}
onComplete?.(fullContent)
return fullContent
}
}
八、安全最佳实践
8.1 输入验证与净化
javascript
class SecurityUtils {
// HTML 转义 - 防止 XSS
static escapeHtml(str) {
const map = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }
return String(str).replace(/[&<>"']/g, c => map[c])
}
// 验证 URL
static isValidUrl(string) {
try {
const url = new URL(string)
return ['http:', 'https:'].includes(url.protocol)
} catch { return false }
}
// 安全 JSON 解析
static safeJsonParse(str, defaultValue = null) {
try { return JSON.parse(str) } catch { return defaultValue }
}
}
8.2 加密存储
javascript
class SecureStorage {
constructor() {
this.encryptionKey = null
}
async init() {
if (this.encryptionKey) return
const result = await chrome.storage.local.get('encryption
Key')
if (result.encryptionKey) {
const keyData = new Uint8Array(result.encryptionKey)
this.encryptionKey = await crypto.subtle.importKey(
'raw', keyData, { name: 'AES-GCM' }, false, ['encrypt', 'decrypt']
)
} else {
this.encryptionKey = await crypto.subtle.generateKey(
{ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']
)
const exported = await crypto.subtle.exportKey('raw', this.encryptionKey)
await chrome.storage.local.set({ encryptionKey: Array.from(new Uint8Array(exported)) })
}
}
async encrypt(data) {
const iv = crypto.getRandomValues(new Uint8Array(12))
const encoded = new TextEncoder().encode(JSON.stringify(data))
const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, this.encryptionKey, encoded)
return { iv: Array.from(iv), data: Array.from(new Uint8Array(encrypted)) }
}
async decrypt(encryptedData) {
const iv = new Uint8Array(encryptedData.iv)
const data = new Uint8Array(encryptedData.data)
const decrypted = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, this.encryptionKey, data)
return JSON.parse(new TextDecoder().decode(decrypted))
}
async setSecure(key, value) {
await this.init()
const encrypted = await this.encrypt(value)
await chrome.storage.local.set({ [key]: encrypted })
}
async getSecure(key) {
await this.init()
const result = await chrome.storage.local.get(key)
if (!result[key]) return null
return this.decrypt(result[key])
}
}
// 使用示例
const secureStorage = new SecureStorage()
await secureStorage.setSecure('apiKey', 'sk-secret-key')
const apiKey = await secureStorage.getSecure('apiKey')
8.3 权限最小化原则
json
{
"permissions": [
"storage", // 必需:数据存储
"activeTab" // 必需:获取当前页面信息
],
"optional_permissions": [
"tabs", // 可选:需要时再请求
"history" // 可选:需要时再请求
],
"host_permissions": [
"https://api.example.com/*" // 只允许访问必要的 API
]
}
九、高级 Chrome API
9.1 declarativeNetRequest API
声明式网络请求拦截,替代 Manifest V2 的 webRequest:
json
// manifest.json
{
"permissions": ["declarativeNetRequest"],
"declarative_net_request": {
"rule_resources": [{ "id": "ruleset_1", "enabled": true, "path": "rules.json" }]
}
}
json
// rules.json
[
{
"id": 1,
"priority": 1,
"action": { "type": "block" },
"condition": {
"urlFilter": "*://ads.example.com/*",
"resourceTypes": ["script", "image"]
}
},
{
"id": 2,
"priority": 2,
"action": {
"type": "modifyHeaders",
"requestHeaders": [{ "header": "X-Custom", "operation": "set", "value": "value" }]
},
"condition": { "urlFilter": "*://api.example.com/*", "resourceTypes": ["xmlhttprequest"] }
}
]
javascript
// 动态添加规则
async function addBlockRule(domain) {
const rules = await chrome.declarativeNetRequest.getDynamicRules()
const nextId = Math.max(0, ...rules.map(r => r.id)) + 1
await chrome.declarativeNetRequest.updateDynamicRules({
addRules: [{
id: nextId,
priority: 1,
action: { type: 'block' },
condition: { urlFilter: `*://${domain}/*`, resourceTypes: ['script'] }
}]
})
}
9.2 Offscreen Documents
在 Manifest V3 中创建隐藏的 DOM 环境:
json
// manifest.json
{ "permissions": ["offscreen"] }
javascript
// background.js
async function createOffscreenDocument() {
const contexts = await chrome.runtime.getContexts({ contextTypes: ['OFFSCREEN_DOCUMENT'] })
if (contexts.length > 0) return
await chrome.offscreen.createDocument({
url: 'offscreen.html',
reasons: ['DOM_PARSER', 'CLIPBOARD'],
justification: '需要解析 HTML 和操作剪贴板'
})
}
// offscreen.js
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.target !== 'offscreen') return
if (message.action === 'parseHTML') {
const parser = new DOMParser()
const doc = parser.parseFromString(message.html, 'text/html')
sendResponse({ title: doc.title, headings: [...doc.querySelectorAll('h1,h2')].map(h => h.textContent) })
}
if (message.action === 'copyToClipboard') {
navigator.clipboard.writeText(message.text).then(() => sendResponse({ success: true }))
return true
}
})
十、国际化支持
10.1 配置国际化
json
// manifest.json
{ "default_locale": "zh_CN" }
json
// _locales/zh_CN/messages.json
{
"extName": { "message": "我的扩展", "description": "扩展名称" },
"extDescription": { "message": "一个强大的浏览器扩展", "description": "扩展描述" },
"buttonSend": { "message": "发送", "description": "发送按钮文本" },
"greeting": { "message": "你好,$USER$!", "placeholders": { "user": { "content": "$1", "example": "张三" } } }
}
json
// _locales/en/messages.json
{
"extName": { "message": "My Extension" },
"extDescription": { "message": "A powerful browser extension" },
"buttonSend": { "message": "Send" },
"greeting": { "message": "Hello, $USER$!", "placeholders": { "user": { "content": "$1" } } }
}
10.2 使用国际化
javascript
// 获取翻译
const name = chrome.i18n.getMessage('extName')
const greeting = chrome.i18n.getMessage('greeting', ['张三'])
// 获取语言
const uiLanguage = chrome.i18n.getUILanguage() // "zh-CN"
// HTML 中使用(需要手动替换)
document.querySelectorAll('[data-i18n]').forEach(el => {
el.textContent = chrome.i18n.getMessage(el.dataset.i18n)
})
html
<!-- HTML 使用 -->
<button data-i18n="buttonSend">Send</button>
<!-- CSS 使用(manifest.json 中的字段) -->
<!-- "__MSG_extName__" 会被自动替换 -->
十一、现代开发工具链
11.1 Vite + CRXJS
bash
npm create vite@latest my-extension -- --template react-ts
cd my-extension
npm install @crxjs/vite-plugin -D
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 })]
})
11.2 TypeScript 配置
json
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM"],
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"types": ["chrome"]
}
}
bash
npm install @types/chrome -D
十二、UI 框架集成
12.1 React 集成
tsx
// src/popup/App.tsx
import { useState, useEffect } from 'react'
export default function App() {
const [settings, setSettings] = useState({ theme: 'light', enabled: true })
useEffect(() => {
chrome.storage.sync.get(['settings'], (result) => {
if (result.settings) setSettings(result.settings)
})
}, [])
const updateSettings = async (updates: Partial<typeof settings>) => {
const newSettings = { ...settings, ...updates }
setSettings(newSettings)
await chrome.storage.sync.set({ settings: newSettings })
}
return (
<div className={`app ${settings.theme}`}>
<h1>扩展设置</h1>
<label>
<input
type="checkbox"
checked={settings.enabled}
onChange={(e) => updateSettings({ enabled: e.target.checked })}
/>
启用扩展
</label>
</div>
)
}
12.2 Vue 3 集成
vue
<!-- src/popup/App.vue -->
<template>
<div :class="['app', settings.theme]">
<h1>扩展设置</h1>
<label>
<input type="checkbox" v-model="settings.enabled" @change="saveSettings" />
启用扩展
</label>
</div>
</template>
<script setup lang="ts">
import { reactive, onMounted } from 'vue'
const settings = reactive({ enabled: true, theme: 'light' })
onMounted(async () => {
const result = await chrome.storage.sync.get(['settings'])
if (result.settings) Object.assign(settings, result.settings)
})
async function saveSettings() {
await chrome.storage.sync.set({ settings: { ...settings } })
}
</script>
12.3 Vue Composables
typescript
// src/composables/useStorage.ts
import { ref, watch, onMounted } from 'vue'
export function useStorage<T>(key: string, defaultValue: T) {
const data = ref<T>(defaultValue)
const loading = ref(true)
onMounted(async () => {
const result = await chrome.storage.local.get(key)
if (result[key] !== undefined) data.value = result[key]
loading.value = false
})
watch(data, async (newValue) => {
await chrome.storage.local.set({ [key]: newValue })
}, { deep: true })
chrome.storage.onChanged.addListener((changes, area) => {
if (area === 'local' && changes[key]) data.value = changes[key].newValue
})
return { data, loading }
}
// 使用
// const { data: settings } = useStorage('settings', { theme: 'light' })
十三、调试技巧
13.1 调试各组件
| 组件 | 调试方法 |
|---|---|
| Background | chrome://extensions/ → 点击 "Service Worker" 链接 |
| Content Script | 目标网页 → F12 → Console/Sources |
| Popup | 右键扩展图标 → 检查弹出内容 |
| Side Panel | 侧边栏内 → 右键 → 检查 |
13.2 常用调试代码
javascript
// 查看扩展信息
console.log(chrome.runtime.getManifest())
// 查看所有存储数据
chrome.storage.local.get(null, console.log)
chrome.storage.sync.get(null, console.log)
// 查看当前标签页
chrome.tabs.query({ active: true, currentWindow: true }, console.log)
// 检查权限
chrome.permissions.getAll(console.log)
13.3 Service Worker 问题排查
javascript
// 保持 Service Worker 活跃
chrome.alarms.create('keepAlive', { periodInMinutes: 0.5 })
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'keepAlive') console.log('Service Worker 保持活跃')
})
// 不要使用全局变量存储状态!
// ❌ 错误
let cachedData = {}
// ✅ 正确:使用 chrome.storage
async function getData(key) {
const result = await chrome.storage.local.get(key)
return result[key]
}
十四、发布与更新
14.1 准备工作
- 注册开发者账号(一次性费用 $5)
- 准备素材:
- 128x128 图标
- 1280x800 或 640x400 截图(1-5 张)
- 440x280 宣传图(可选)
- 详细描述和隐私政策
14.2 打包扩展
bash
cd dist
zip -r ../extension.zip . -x "*.git*" -x "node_modules/*"
14.3 发布流程
- 访问 Chrome Web Store Developer Dashboard
- 点击"新建商品"
- 上传 ZIP 文件
- 填写商品详情
- 提交审核(通常 1-3 天)
十五、监控与分析
15.1 错误追踪
javascript
/**
* 错误追踪系统
*/
class ErrorTracker {
constructor(options = {}) {
this.endpoint = options.endpoint
this.maxErrors = options.maxErrors || 100
this.errors = []
this.setupGlobalHandlers()
}
setupGlobalHandlers() {
// 捕获未处理的错误
self.addEventListener('error', (event) => {
this.capture({
type: 'uncaught_error',
message: event.message,
filename: event.filename,
lineno: event.lineno,
stack: event.error?.stack
})
})
// 捕获未处理的 Promise 拒绝
self.addEventListener('unhandledrejection', (event) => {
this.capture({
type: 'unhandled_rejection',
reason: event.reason?.message || String(event.reason),
stack: event.reason?.stack
})
})
}
capture(errorInfo) {
const error = {
...errorInfo,
timestamp: Date.now(),
url: location.href,
userAgent: navigator.userAgent,
extensionVersion: chrome.runtime.getManifest().version
}
this.errors.push(error)
if (this.errors.length > this.maxErrors) this.errors.shift()
this.saveToStorage()
if (this.endpoint) this.sendToServer(error)
console.error('[ErrorTracker]', error)
}
async saveToStorage() {
await chrome.storage.local.set({ errorLogs: this.errors })
}
async sendToServer(error) {
try {
await fetch(this.endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(error)
})
} catch (e) {
console.warn('发送错误日志失败:', e)
}
}
getErrors() { return this.errors }
clearErrors() { this.errors = []; this.saveToStorage() }
}
// 初始化
const errorTracker = new ErrorTracker({
endpoint: 'https://api.example.com/errors',
maxErrors: 50
})
15.2 用户行为分析
javascript
/**
* 用户行为分析
*/
class Analytics {
constructor(options = {}) {
this.enabled = options.enabled ?? true
this.events = []
this.sessionId = `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
this.startTime = Date.now()
}
track(eventName, properties = {}) {
if (!this.enabled) return
const event = {
name: eventName,
properties,
timestamp: Date.now(),
sessionId: this.sessionId,
sessionDuration: Date.now() - this.startTime
}
this.events.push(event)
this.saveToStorage()
console.log('[Analytics] 事件:', eventName, properties)
}
// 常用事件追踪
trackPageView(page) { this.track('page_view', { page }) }
trackButtonClick(buttonId, buttonText) { this.track('button_click', { buttonId, buttonText }) }
trackFeatureUse(feature) { this.track('feature_use', { feature }) }
trackAPICall(endpoint, duration, success) { this.track('api_call', { endpoint, duration, success }) }
async saveToStorage() {
const recentEvents = this.events.slice(-1000)
await chrome.storage.local.set({ analyticsEvents: recentEvents })
}
async getStats() {
const events = this.events
return {
totalEvents: events.length,
eventsByType: events.reduce((acc, e) => ({ ...acc, [e.name]: (acc[e.name] || 0) + 1 }), {}),
sessionsCount: new Set(events.map(e => e.sessionId)).size
}
}
}
// 使用示例
const analytics = new Analytics({ enabled: true })
analytics.trackPageView('sidebar')
analytics.trackFeatureUse('ai-chat')
15.3 性能监控
javascript
/**
* 性能监控
*/
class PerformanceMonitor {
constructor() {
this.metrics = []
}
async measure(name, fn) {
const start = performance.now()
try {
const result = await fn()
this.record(name, performance.now() - start, true)
return result
} catch (error) {
this.record(name, performance.now() - start, false, error.message)
throw error
}
}
record(name, duration, success, error = null) {
this.metrics.push({ name, duration, success, error, timestamp: Date.now() })
if (this.metrics.length > 500) this.metrics.shift()
}
getAverageTime(name) {
const relevant = this.metrics.filter(m => m.name === name)
if (!relevant.length) return 0
return relevant.reduce((sum, m) => sum + m.duration, 0) / relevant.length
}
getSuccessRate(name) {
const relevant = this.metrics.filter(m => m.name === name)
if (!relevant.length) return 0
return (relevant.filter(m => m.success).length / relevant.length) * 100
}
getReport() {
const names = [...new Set(this.metrics.map(m => m.name))]
return names.map(name => ({
name,
count: this.metrics.filter(m => m.name === name).length,
averageTime: this.getAverageTime(name).toFixed(2) + 'ms',
successRate: this.getSuccessRate(name).toFixed(1) + '%'
}))
}
}
// 使用示例
const perfMonitor = new PerformanceMonitor()
// 测量 API 调用
const response = await perfMonitor.measure('api_chat', async () => {
return await fetch('/api/chat', { method: 'POST', body: JSON.stringify(data) })
})
// 获取性能报告
console.table(perfMonitor.getReport())
十六、最佳实践总结
16.1 安全建议
- 不要使用
eval()或动态执行远程代码 - 验证所有输入,防止 XSS 攻击
- 最小权限原则,只申请必要的权限
- 加密敏感数据 存储前进行加密
16.2 性能优化
- Service Worker 休眠处理 :使用
chrome.alarms保持活跃 - 批量存储操作:合并多次写入
- 懒加载:按需加载模块
- 使用 IndexedDB:存储大量数据
16.3 用户体验
- 提供设置页面:让用户自定义行为
- 国际化:支持多语言
- 优雅降级:处理权限被拒绝的情况
- 清晰的错误提示:帮助用户理解问题
十七、常见问题与解决方案
Q1: Service Worker 频繁休眠怎么办?
javascript
chrome.alarms.create('keepAlive', { periodInMinutes: 0.5 })
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'keepAlive') console.log('保持活跃')
})
Q2: 消息发送后没有响应?
javascript
// 确保返回 true 以支持异步响应
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
handleAsync(message).then(sendResponse)
return true // ← 这很重要!
})
Q3: Content Script 无法接收消息?
javascript
// 确保 Content Script 已注入
async function sendToContentScript(tabId, message) {
try {
await chrome.scripting.executeScript({
target: { tabId },
files: ['content-script.js']
})
} catch {}
return chrome.tabs.sendMessage(tabId, message)
}
Q4: 如何在 Service Worker 中持久化数据?
javascript
// 不要使用全局变量,使用 chrome.storage
async function getData(key) {
const result = await chrome.storage.local.get(key)
return result[key]
}
十八、参考资源
官方文档
开发工具
常用 Chrome API 速查
| API | 用途 | 权限 |
|---|---|---|
| chrome.runtime | 扩展生命周期、消息通信 | - |
| chrome.storage | 数据存储 | storage |
| chrome.tabs | 标签页管理 | tabs |
| chrome.sidePanel | 侧边栏 | sidePanel |
| chrome.action | 工具栏图标 | - |
| chrome.contextMenus | 右键菜单 | contextMenus |
| chrome.notifications | 系统通知 | notifications |
| chrome.alarms | 定时器 | alarms |
| chrome.scripting | 脚本注入 | scripting |
| chrome.declarativeNetRequest | 网络请求拦截 | declarativeNetRequest |
| chrome.offscreen | 离屏文档 | offscreen |
| chrome.i18n | 国际化 | - |
总结
本文系统性地介绍了 Chrome 扩展开发的核心知识:
- 项目结构:manifest.json 配置、各文件职责
- 核心组件:Background Service Worker、Content Script、Side Panel、Popup
- 消息通信:runtime.sendMessage、tabs.sendMessage、Port 长连接
- 数据存储:chrome.storage API、IndexedDB
- 网络请求:HTTP 封装、流式 AI 对话
- 安全实践:输入验证、加密存储、权限最小化
- 高级 API:declarativeNetRequest、Offscreen Documents
- 国际化:i18n 支持
- 现代工具链:Vite + CRXJS、TypeScript、React/Vue
- 监控与分析:错误追踪、用户行为分析、性能监控
- 发布流程:Chrome Web Store 发布步骤
本文基于实际项目经验整理,代码示例均经过测试验证。如有问题欢迎交流讨论。