浏览器插件开发 (Browser Extension)
Chrome/Edge 扩展开发核心概念与 Manifest V3 最佳实践
目录
核心概念
插件架构
┌─────────────────────────────────────────────────────┐
│ Browser Extension │
├─────────────┬─────────────┬─────────────────────────┤
│ Popup │ Options │ DevTools Panel │
│ (HTML) │ (HTML) │ (HTML) │
├─────────────┴─────────────┴─────────────────────────┤
│ Service Worker (Background) │
│ (事件驱动,无 DOM 访问) │
├─────────────────────────────────────────────────────┤
│ Content Scripts │
│ (注入到网页,可访问 DOM) │
└─────────────────────────────────────────────────────┘核心组件职责
| 组件 | 运行环境 | 访问能力 | 生命周期 |
|---|---|---|---|
| Service Worker | 独立 V8 | Chrome APIs | 按需唤醒 |
| Content Script | 网页 DOM | DOM + 部分 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 V2 | Manifest V3 |
|---|---|---|
| 后台脚本 | background.page/scripts | service_worker |
| 远程代码 | 允许 | 禁止 |
| Web Request | 阻塞式 | declarativeNetRequest |
| 主机权限 | permissions | host_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 });
}
});Popup
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}`);
}
});对比表
| 类型 | 容量 | 同步 | 持久化 |
|---|---|---|---|
local | 5MB | ❌ | ✅ |
sync | 100KB | ✅ 跨设备 | ✅ |
session | 10MB | ❌ | ❌ |
权限系统
权限分类
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?
- 性能优化:Service Worker 按需唤醒,不常驻内存
- 安全性:禁止远程代码执行,减少攻击面
- 资源节省:非活跃时自动挂起
挑战:
- 无持久状态,需使用
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 Worker | chrome://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