Chrome 扩展开发指南:从入门到精通 Manifest V3

14 阅读10分钟

本文基于 Manifest V3 标准,系统性地介绍 Chrome 浏览器扩展开发的核心知识,涵盖项目架构、核心组件、消息通信、存储方案、网络请求、安全实践、现代工具链集成等内容,并提供大量可运行的代码示例。

前言

Chrome 扩展是一种能够扩展浏览器功能的小型程序。它可以修改网页内容、添加新功能、与 Web 服务交互,甚至构建完整的应用程序。随着 Manifest V3 的全面推行,扩展开发迎来了重大变革:Background Pages 被 Service Workers 取代,网络请求拦截改用 declarativeNetRequest,安全策略更加严格。

特性Manifest V2Manifest V3
后台脚本Background PagesService Workers
远程代码允许禁止
eval()允许禁止
网络请求拦截webRequest (blocking)declarativeNetRequest
内容安全策略较宽松更严格
Host Permissions在 permissions 中单独的 host_permissions
Promise 支持部分支持全面支持

本文将带你全面掌握现代 Chrome 扩展开发。

一、快速开始

1.1 最小可运行扩展

一个 Chrome 扩展至少需要一个 manifest.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

<!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 加载扩展

  1. 打开 chrome://extensions/
  2. 启用右上角的"开发者模式"
  3. 点击"加载已解压的扩展程序"
  4. 选择项目目录

二、项目结构

2.1 标准项目结构

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 架构概览图

┌───────────────────────────────────────────────────────────────────────────┐
│                            浏览器扩展架构图                                  │
├───────────────────────────────────────────────────────────────────────────┤
│                                                                           │
│  ┌─────────────────┐    消息通信      ┌─────────────────┐                  │
│  │   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 完整配置示例

{
  "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

// 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系统通知
cookiesCookie 管理
history浏览历史
bookmarks书签管理
downloads下载管理
offscreen离屏文档
declarativeNetRequest网络请求拦截

四、核心组件详解

4.1 Background Service Worker

Service Worker 是扩展的"大脑",负责事件监听、状态管理和跨组件通信。

// 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。

// 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(侧边栏)

<!-- 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>
// 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 通信架构图

┌──────────┐    postMessage    ┌──────────────┐   chrome.runtime   ┌──────────────┐
│ Web Page │ ◄──────────────►  │Content Script│ ◄────────────────► │  Background  │
└──────────┘                   └──────────────┘                    └──────────────┘
                                      ▲                                  ▲
                                      │     chrome.runtime.sendMessage   │
                                      ▼                                  │
                               ┌──────────────┐                          │
                               │ Popup/Sidebar│ ◄────────────────────────┘
                               └──────────────┘     chrome.tabs.sendMessage

5.2 消息发送模式

// 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)

// 建立连接
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

// 本地存储(无大小限制,不同步)
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 封装存储工具类

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 请求封装

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 对话

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 输入验证与净化

class SecurityUtils {
  // HTML 转义 - 防止 XSS
  static escapeHtml(str) {
    const map = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#x27;' }
    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 加密存储

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 权限最小化原则

{
  "permissions": [
    "storage",    // 必需:数据存储
    "activeTab"   // 必需:获取当前页面信息
  ],
  "optional_permissions": [
    "tabs",       // 可选:需要时再请求
    "history"     // 可选:需要时再请求
  ],
  "host_permissions": [
    "https://api.example.com/*"  // 只允许访问必要的 API
  ]
}

九、高级 Chrome API

9.1 declarativeNetRequest API

声明式网络请求拦截,替代 Manifest V2 的 webRequest:

// manifest.json
{
  "permissions": ["declarativeNetRequest"],
  "declarative_net_request": {
    "rule_resources": [{ "id": "ruleset_1", "enabled": true, "path": "rules.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"] }
  }
]
// 动态添加规则
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 环境:

// manifest.json
{ "permissions": ["offscreen"] }
// 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 配置国际化

// manifest.json
{ "default_locale": "zh_CN" }
// _locales/zh_CN/messages.json
{
  "extName": { "message": "我的扩展", "description": "扩展名称" },
  "extDescription": { "message": "一个强大的浏览器扩展", "description": "扩展描述" },
  "buttonSend": { "message": "发送", "description": "发送按钮文本" },
  "greeting": { "message": "你好,$USER$!", "placeholders": { "user": { "content": "$1", "example": "张三" } } }
}
// _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 使用国际化

// 获取翻译
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 使用 -->
<button data-i18n="buttonSend">Send</button>

<!-- CSS 使用(manifest.json 中的字段) -->
<!-- "__MSG_extName__" 会被自动替换 -->

十一、现代开发工具链

11.1 Vite + CRXJS

npm create vite@latest my-extension -- --template react-ts
cd my-extension
npm install @crxjs/vite-plugin -D
// 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 配置

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["ES2022", "DOM"],
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "types": ["chrome"]
  }
}
npm install @types/chrome -D

十二、UI 框架集成

12.1 React 集成

// 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 集成

<!-- 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

// 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 调试各组件

组件调试方法
Backgroundchrome://extensions/ → 点击 "Service Worker" 链接
Content Script目标网页 → F12 → Console/Sources
Popup右键扩展图标 → 检查弹出内容
Side Panel侧边栏内 → 右键 → 检查

13.2 常用调试代码

// 查看扩展信息
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 问题排查

// 保持 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 准备工作

  1. 注册开发者账号(一次性费用 $5)
  2. 准备素材:
    • 128x128 图标
    • 1280x800 或 640x400 截图(1-5 张)
    • 440x280 宣传图(可选)
    • 详细描述和隐私政策

14.2 打包扩展

cd dist
zip -r ../extension.zip . -x "*.git*" -x "node_modules/*"

14.3 发布流程

  1. 访问 Chrome Web Store Developer Dashboard
  2. 点击"新建商品"
  3. 上传 ZIP 文件
  4. 填写商品详情
  5. 提交审核(通常 1-3 天)

十五、监控与分析

15.1 错误追踪

/**
 * 错误追踪系统
 */
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 用户行为分析

/**
 * 用户行为分析
 */
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 性能监控

/**
 * 性能监控
 */
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 频繁休眠怎么办?

chrome.alarms.create('keepAlive', { periodInMinutes: 0.5 })
chrome.alarms.onAlarm.addListener((alarm) => {
  if (alarm.name === 'keepAlive') console.log('保持活跃')
})

Q2: 消息发送后没有响应?

// 确保返回 true 以支持异步响应
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  handleAsync(message).then(sendResponse)
  return true // ← 这很重要!
})

Q3: Content Script 无法接收消息?

// 确保 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 中持久化数据?

// 不要使用全局变量,使用 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 扩展开发的核心知识:

  1. 项目结构:manifest.json 配置、各文件职责
  2. 核心组件:Background Service Worker、Content Script、Side Panel、Popup
  3. 消息通信:runtime.sendMessage、tabs.sendMessage、Port 长连接
  4. 数据存储:chrome.storage API、IndexedDB
  5. 网络请求:HTTP 封装、流式 AI 对话
  6. 安全实践:输入验证、加密存储、权限最小化
  7. 高级 API:declarativeNetRequest、Offscreen Documents
  8. 国际化:i18n 支持
  9. 现代工具链:Vite + CRXJS、TypeScript、React/Vue
  10. 监控与分析:错误追踪、用户行为分析、性能监控
  11. 发布流程:Chrome Web Store 发布步骤

本文基于实际项目经验整理,代码示例均经过测试验证。如有问题欢迎交流讨论。