Skip to content

浏览器插件开发 (Browser Extension)

Chrome/Edge 扩展开发核心概念与 Manifest V3 最佳实践

目录


核心概念

插件架构

┌─────────────────────────────────────────────────────┐
│                    Browser Extension                 │
├─────────────┬─────────────┬─────────────────────────┤
│   Popup     │   Options   │    DevTools Panel       │
│   (HTML)    │   (HTML)    │       (HTML)            │
├─────────────┴─────────────┴─────────────────────────┤
│              Service Worker (Background)             │
│          (事件驱动,无 DOM 访问)                      │
├─────────────────────────────────────────────────────┤
│              Content Scripts                         │
│          (注入到网页,可访问 DOM)                     │
└─────────────────────────────────────────────────────┘

核心组件职责

组件运行环境访问能力生命周期
Service Worker独立 V8Chrome APIs按需唤醒
Content Script网页 DOMDOM + 部分 APIs页面加载时
Popup独立窗口Chrome APIs点击图标时
Options独立标签页Chrome APIs用户打开时

Manifest V3 配置

基础结构

json
{
  "manifest_version": 3,
  "name": "My Extension",
  "version": "1.0.0",
  "description": "A sample extension",
  
  "permissions": ["storage", "activeTab"],
  "host_permissions": ["https://*.example.com/*"],
  
  "background": {
    "service_worker": "background.js",
    "type": "module"
  },
  
  "content_scripts": [{
    "matches": ["https://*.example.com/*"],
    "js": ["content.js"],
    "css": ["content.css"],
    "run_at": "document_idle"
  }],
  
  "action": {
    "default_popup": "popup.html",
    "default_icon": {
      "16": "icons/icon16.png",
      "48": "icons/icon48.png",
      "128": "icons/icon128.png"
    }
  },
  
  "options_page": "options.html",
  
  "icons": {
    "16": "icons/icon16.png",
    "48": "icons/icon48.png",
    "128": "icons/icon128.png"
  }
}

V2 → V3 主要变化

特性Manifest V2Manifest V3
后台脚本background.page/scriptsservice_worker
远程代码允许禁止
Web Request阻塞式declarativeNetRequest
主机权限permissionshost_permissions
Promise回调原生 Promise

核心组件

Service Worker (Background)

javascript
// background.js
// 注意:无 DOM 访问,无 window 对象

// 安装事件
chrome.runtime.onInstalled.addListener((details) => {
  if (details.reason === 'install') {
    chrome.storage.local.set({ initialized: true });
  }
});

// 监听消息
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.type === 'GET_DATA') {
    fetchData().then(data => sendResponse({ data }));
    return true; // 保持消息通道开启
  }
});

// 定时任务 (替代 setInterval)
chrome.alarms.create('refresh', { periodInMinutes: 30 });
chrome.alarms.onAlarm.addListener((alarm) => {
  if (alarm.name === 'refresh') {
    refreshData();
  }
});

Content Script

javascript
// content.js
// 运行在网页上下文,可访问 DOM

// 注入时执行
console.log('Content script loaded on:', window.location.href);

// 操作 DOM
const button = document.createElement('button');
button.textContent = 'Click me';
button.addEventListener('click', () => {
  chrome.runtime.sendMessage({ type: 'BUTTON_CLICKED' });
});
document.body.appendChild(button);

// 监听来自 background 的消息
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.type === 'GET_PAGE_TITLE') {
    sendResponse({ title: document.title });
  }
});
html
<!-- popup.html -->
<!DOCTYPE html>
<html>
<head>
  <style>
    body { width: 300px; padding: 16px; }
    button { width: 100%; padding: 8px; }
  </style>
</head>
<body>
  <h1>My Extension</h1>
  <button id="action-btn">Do Something</button>
  <script src="popup.js"></script>
</body>
</html>
javascript
// popup.js
document.getElementById('action-btn').addEventListener('click', async () => {
  // 向当前标签页的 content script 发送消息
  const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
  const response = await chrome.tabs.sendMessage(tab.id, { type: 'GET_PAGE_TITLE' });
  console.log('Page title:', response.title);
});

消息通信

通信模式

┌─────────┐     runtime.sendMessage     ┌──────────────────┐
│  Popup  │ ◄──────────────────────────► │ Service Worker   │
└─────────┘     runtime.onMessage        └──────────────────┘
     │                                            │
     │                                            │
     │ tabs.sendMessage                           │ tabs.sendMessage
     │ runtime.onMessage                          │ runtime.onMessage
     ▼                                            ▼
┌─────────────────────────────────────────────────────────────┐
│                      Content Script                          │
└─────────────────────────────────────────────────────────────┘

一次性消息

javascript
// 发送方
chrome.runtime.sendMessage({ type: 'HELLO' }, (response) => {
  console.log('Response:', response);
});

// 接收方
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.type === 'HELLO') {
    sendResponse({ reply: 'Hi there!' });
  }
  return false; // 同步响应
});

长连接

javascript
// content.js - 发起连接
const port = chrome.runtime.connect({ name: 'data-channel' });

port.postMessage({ type: 'SUBSCRIBE' });
port.onMessage.addListener((message) => {
  console.log('Received:', message);
});

// background.js - 接收连接
chrome.runtime.onConnect.addListener((port) => {
  if (port.name === 'data-channel') {
    port.onMessage.addListener((message) => {
      if (message.type === 'SUBSCRIBE') {
        // 推送数据
        setInterval(() => {
          port.postMessage({ data: Date.now() });
        }, 1000);
      }
    });
  }
});

与网页通信

javascript
// content.js
window.addEventListener('message', (event) => {
  if (event.source !== window) return;
  if (event.data.type === 'FROM_PAGE') {
    // 转发给 background
    chrome.runtime.sendMessage(event.data);
  }
});

// 发送给网页
window.postMessage({ type: 'FROM_EXTENSION', data: 'hello' }, '*');

// 网页脚本
window.addEventListener('message', (event) => {
  if (event.data.type === 'FROM_EXTENSION') {
    console.log('From extension:', event.data);
  }
});

存储 API

chrome.storage

javascript
// 本地存储(每个扩展 5MB)
chrome.storage.local.set({ key: 'value' });
chrome.storage.local.get(['key'], (result) => {
  console.log(result.key);
});

// 同步存储(每项 8KB,总共 100KB)
chrome.storage.sync.set({ settings: { theme: 'dark' } });

// 会话存储(Service Worker 唤醒期间)
chrome.storage.session.set({ temp: 'data' });

// 监听变化
chrome.storage.onChanged.addListener((changes, area) => {
  for (const [key, { oldValue, newValue }] of Object.entries(changes)) {
    console.log(`${area}: ${key} changed from ${oldValue} to ${newValue}`);
  }
});

对比表

类型容量同步持久化
local5MB
sync100KB✅ 跨设备
session10MB

权限系统

权限分类

json
{
  "permissions": [
    "storage",      // 存储 API
    "activeTab",    // 当前标签页(用户主动触发)
    "alarms",       // 定时器
    "notifications" // 通知
  ],
  "optional_permissions": [
    "tabs",         // 读取所有标签页信息
    "bookmarks"     // 书签管理
  ],
  "host_permissions": [
    "https://*.example.com/*"
  ]
}

动态请求权限

javascript
// 检查权限
const hasPermission = await chrome.permissions.contains({
  permissions: ['tabs']
});

// 请求权限(必须由用户手势触发)
document.getElementById('btn').addEventListener('click', async () => {
  const granted = await chrome.permissions.request({
    permissions: ['tabs'],
    origins: ['https://*.google.com/*']
  });
  
  if (granted) {
    console.log('Permission granted');
  }
});

高频面试题

Q1: Manifest V3 为什么移除 Background Page?

  1. 性能优化:Service Worker 按需唤醒,不常驻内存
  2. 安全性:禁止远程代码执行,减少攻击面
  3. 资源节省:非活跃时自动挂起

挑战

  • 无持久状态,需使用 chrome.storage
  • 无 DOM 访问,无法使用 XMLHttpRequest(改用 fetch

Q2: Content Script 和网页脚本的隔离机制?

特性Content Script网页脚本
JS 环境独立(隔离世界)网页
DOM 访问✅ 共享
window 对象独立副本原始
Chrome APIs部分

通信方式window.postMessage 或 DOM CustomEvent

Q3: 如何在 Service Worker 中实现定时任务?

javascript
// ❌ 错误:setInterval 不可靠,SW 会挂起
setInterval(() => {}, 60000);

// ✅ 正确:使用 chrome.alarms
chrome.alarms.create('myAlarm', {
  delayInMinutes: 1,     // 首次触发延迟
  periodInMinutes: 30    // 重复间隔
});

chrome.alarms.onAlarm.addListener((alarm) => {
  if (alarm.name === 'myAlarm') {
    doSomething();
  }
});

Q4: 如何调试浏览器插件?

组件调试方式
Service Workerchrome://extensions → 背景页链接
Content Script网页 DevTools → Sources
Popup右键 Popup → 检查

项目结构示例

my-extension/
├── manifest.json
├── background.js          # Service Worker
├── content/
│   ├── content.js         # Content Script
│   └── content.css
├── popup/
│   ├── popup.html
│   ├── popup.js
│   └── popup.css
├── options/
│   ├── options.html
│   └── options.js
├── icons/
│   ├── icon16.png
│   ├── icon48.png
│   └── icon128.png
└── lib/                   # 公共模块
    └── utils.js

前端面试知识库