引言:为什么现在要学Chrome插件开发?
Chrome浏览器插件(Chrome Extensions)是提升浏览器功能、定制Web体验的利器。2024年Google全面强制迁移至Manifest V3,废弃了MV2的background page机制,转而使用Service Worker。这意味着如果你还在用MV2的写法,你的插件已经在Chrome Web Store上无法更新了。本文将从零开始,手把手教你构建一个符合Manifest V3标准的现代化Chrome插件,涵盖核心API、消息通信、存储方案、Side Panel以及实际发布流程。
一、Manifest V3架构概览
Manifest V3(简称MV3)是Chrome插件的最新架构规范,相比MV2有几个根本性变化:
- Service Worker替代Background Page:不再有持久运行的后台页面,取而代之的是事件驱动的Service Worker,空闲时会被回收
- 移除远程代码执行:所有代码必须打包进插件,不允许从外部加载和执行JavaScript
- declarativeNetRequest替代webRequest:网络请求拦截改为声明式API,减少性能开销
- 新的权限模型:权限粒度更细,支持运行时权限请求
一个标准的MV3插件目录结构如下:
my-extension/
├── manifest.json # 插件配置清单(必需)
├── background.js # Service Worker入口
├── content.js # 注入到网页的内容脚本
├── popup.html # 点击插件图标弹出的面板
├── popup.js # popup的逻辑
├── popup.css # popup的样式
├── sidepanel.html # 侧边栏面板(MV3新增)
├── sidepanel.js # 侧边栏逻辑
├── options.html # 设置页面
├── icons/
│ ├── icon16.png
│ ├── icon48.png
│ └── icon128.png
└── _locales/ # 国际化资源
├── zh_CN/
│ └── messages.json
└── en/
└── messages.json
二、manifest.json深度解析
manifest.json是Chrome插件的核心配置文件,每个字段都有明确的作用。下面是一个完整的MV3 manifest示例:
{
"manifest_version": 3,
"name": "__MSG_extName__",
"version": "1.0.0",
"description": "__MSG_extDesc__",
"default_locale": "zh_CN",
"icons": {
"16": "icons/icon16.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
},
"permissions": [
"storage",
"activeTab",
"scripting",
"sidePanel",
"contextMenus",
"notifications",
"alarms"
],
"host_permissions": [
"https://*.example.com/*"
],
"background": {
"service_worker": "background.js",
"type": "module"
},
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "icons/icon16.png",
"48": "icons/icon48.png"
},
"default_title": "点击打开面板"
},
"content_scripts": [
{
"matches": ["https://*.example.com/*"],
"js": ["content.js"],
"css": ["content.css"],
"run_at": "document_idle"
}
],
"side_panel": {
"default_path": "sidepanel.html"
},
"options_page": "options.html",
"minimum_chrome_version": "116",
"web_accessible_resources": [
{
"resources": ["images/*", "fonts/*"],
"matches": [""]
}
]
}
关键字段说明:
- manifest_version:必须是3,这是MV3的标识
- permissions:声明插件需要的Chrome API权限,每个权限在安装时会向用户展示
- host_permissions:控制插件可以访问哪些网站的数据,MV3中从permissions中分离出来
- background.service_worker:指定Service Worker的入口文件,注意MV3只支持一个入口
- content_scripts:配置自动注入到匹配网页中的脚本和样式
- type: “module”:允许在Service Worker中使用ES Module语法import/export
三、Service Worker:插件的大脑
MV3的Service Worker是插件的事件处理中心。与Web Service Worker类似,它不常驻内存,而是在收到事件时被唤起,处理完成后自动休眠。这意味着你需要特别注意状态管理——不能把可变状态存在Service Worker的全局变量中。
// background.js — Service Worker入口
// 使用chrome.alarms代替setTimeout,因为SW休眠后setTimeout会失效
// 插件安装时的初始化
chrome.runtime.onInstalled.addListener((details) => {
console.log('插件已安装,原因:', details.reason);
// 创建右键菜单
chrome.contextMenus.create({
id: 'search-selection',
title: '使用自定义搜索: "%s"',
contexts: ['selection']
});
// 设置默认配置
chrome.storage.local.set({
settings: {
theme: 'light',
enableNotifications: true,
apiEndpoint: 'https://api.example.com'
}
});
// 创建定时任务
chrome.alarms.create('sync-data', { periodInMinutes: 30 });
});
// 处理右键菜单点击
chrome.contextMenus.onClicked.addListener((info, tab) => {
if (info.menuItemId === 'search-selection') {
const query = encodeURIComponent(info.selectionText);
chrome.tabs.create({
url: `https://www.google.com/search?q=${query}`
});
}
});
// 处理定时任务
chrome.alarms.onAlarm.addListener((alarm) => {
if (alarm.name === 'sync-data') {
syncRemoteData();
}
});
// 处理来自content script和popup的消息
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
switch (message.type) {
case 'GET_DATA':
handleGetData(message.payload).then(sendResponse);
return true; // 保持消息通道开放,等待异步响应
case 'SAVE_TAB_INFO':
saveTabInfo(sender.tab, message.payload);
sendResponse({ success: true });
break;
case 'OPEN_SIDE_PANEL':
chrome.sidePanel.open({ tabId: sender.tab.id });
sendResponse({ success: true });
break;
}
});
// 监听标签页更新事件
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (changeInfo.status === 'complete' && tab.url) {
if (tab.url.includes('github.com')) {
chrome.tabs.sendMessage(tabId, {
type: 'PAGE_READY',
url: tab.url
}).catch(() => {});
}
}
});
async function handleGetData(payload) {
const data = await chrome.storage.local.get(payload.keys);
return { success: true, data };
}
async function saveTabInfo(tab, info) {
const { tabHistory = [] } = await chrome.storage.local.get('tabHistory');
tabHistory.push({
url: tab.url,
title: tab.title,
info,
timestamp: Date.now()
});
if (tabHistory.length > 100) tabHistory.splice(0, tabHistory.length - 100);
await chrome.storage.local.set({ tabHistory });
}
async function syncRemoteData() {
try {
const { settings } = await chrome.storage.local.get('settings');
const response = await fetch(`${settings.apiEndpoint}/sync`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
const data = await response.json();
await chrome.storage.local.set({ syncedData: data });
} catch (error) {
console.error('同步失败:', error);
}
}
3.1 Service Worker的生命周期陷阱
Service Worker在空闲约30秒后会被Chrome终止。以下是你必须注意的几个陷阱:
- 不要用全局变量存储状态:SW被杀死后全局变量会丢失,使用chrome.storage代替
- 不要用setTimeout/setInterval:SW休眠后这些定时器会失效,使用chrome.alarms代替
- 长时间操作需要保活:可以使用chrome.runtime.Port或者定期发送消息来延长SW的生命周期
- 异步操作要小心:使用async/await确保操作完成后再返回
四、Content Script:网页中的代理人
Content Script运行在网页的上下文中,可以读取和修改DOM,但它运行在独立的JavaScript沙箱中,无法直接访问网页的全局变量。通过Isolated World机制,你的脚本与网页脚本互不干扰。
// content.js — 注入到目标网页的内容脚本
// 检查是否已经注入过(防止重复注入)
if (!window.__myExtensionInjected) {
window.__myExtensionInjected = true;
initContentScript();
}
function initContentScript() {
console.log('[MyExtension] Content script已加载');
// 1. 读取页面信息
const pageInfo = {
title: document.title,
url: window.location.href,
meta: getMetaTags(),
headings: getHeadings()
};
// 2. 向Service Worker发送页面信息
chrome.runtime.sendMessage({
type: 'SAVE_TAB_INFO',
payload: pageInfo
});
// 3. 注入自定义UI元素
injectFloatingButton();
// 4. 监听来自Service Worker的消息
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.type === 'PAGE_READY') {
console.log('[MyExtension] 收到页面就绪通知:', msg.url);
sendResponse({ received: true });
}
if (msg.type === 'HIGHLIGHT_TEXT') {
highlightMatches(msg.payload.keyword);
sendResponse({ success: true });
}
});
// 5. 使用MutationObserver监听DOM变化
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
processNewElement(node);
}
});
}
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
}
function getMetaTags() {
const metas = {};
document.querySelectorAll('meta[name], meta[property]').forEach(el => {
const key = el.getAttribute('name') || el.getAttribute('property');
metas[key] = el.getAttribute('content');
});
return metas;
}
function getHeadings() {
return Array.from(document.querySelectorAll('h1, h2, h3')).map(h => ({
level: h.tagName,
text: h.textContent.trim()
}));
}
function injectFloatingButton() {
const btn = document.createElement('div');
btn.id = 'my-ext-floating-btn';
btn.innerHTML = '🔧';
btn.style.cssText = `
position: fixed; bottom: 20px; right: 20px; z-index: 2147483647;
width: 48px; height: 48px; border-radius: 50%;
background: #4285f4; color: white; font-size: 24px;
display: flex; align-items: center; justify-content: center;
cursor: pointer; box-shadow: 0 2px 10px rgba(0,0,0,0.3);
transition: transform 0.2s;
`;
btn.addEventListener('mouseenter', () => btn.style.transform = 'scale(1.1)');
btn.addEventListener('mouseleave', () => btn.style.transform = 'scale(1)');
btn.addEventListener('click', () => {
chrome.runtime.sendMessage({ type: 'OPEN_SIDE_PANEL' });
});
document.body.appendChild(btn);
}
function highlightMatches(keyword) {
document.querySelectorAll('.my-ext-highlight').forEach(el => {
el.replaceWith(el.textContent);
});
if (!keyword) return;
const walker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_TEXT,
null,
false
);
const textNodes = [];
while (walker.nextNode()) textNodes.push(walker.currentNode);
textNodes.forEach(node => {
const idx = node.textContent.toLowerCase().indexOf(keyword.toLowerCase());
if (idx !== -1) {
const range = document.createRange();
range.setStart(node, idx);
range.setEnd(node, idx + keyword.length);
const span = document.createElement('span');
span.className = 'my-ext-highlight';
span.style.cssText = 'background: #ffeb3b; padding: 1px 2px; border-radius: 2px;';
range.surroundContents(span);
}
});
}
function processNewElement(element) {
// 对新增元素执行自定义逻辑
}
4.1 Content Script与网页通信
Content Script不能直接访问网页的JavaScript变量,但可以通过window.postMessage进行通信。这在需要与嵌入的iframe或者网页自定义脚本交互时非常有用:
// 在content.js中向网页发送消息
window.postMessage({ type: 'FROM_EXTENSION', data: 'hello' }, '*');
// 在content.js中监听网页的消息
window.addEventListener('message', (event) => {
// 安全检查:只处理来自同源的消息
if (event.source !== window) return;
if (event.data.type && event.data.type === 'FROM_PAGE') {
console.log('收到来自网页的消息:', event.data.payload);
// 转发给Service Worker
chrome.runtime.sendMessage({
type: 'PAGE_MESSAGE',
payload: event.data.payload
});
}
});
五、Chrome Storage API:数据持久化方案
MV3提供了三种存储方案,各有适用场景:
| API | 存储位置 | 容量 | 同步 | 适用场景 |
|---|---|---|---|---|
| chrome.storage.local | 本地磁盘 | 10MB(可申请unlimitedStorage) | 否 | 大量本地数据缓存 |
| chrome.storage.sync | Google账号 | 100KB(单项8KB) | 跨设备同步 | 用户设置、偏好 |
| chrome.storage.session | 内存 | 10MB | 否(SW重启后丢失) | 临时状态数据 |
// storage.js — 统一的存储管理模块
class ExtensionStorage {
// 保存数据
static async set(key, value, area = 'local') {
const storage = chrome.storage[area];
return new Promise((resolve, reject) => {
storage.set({ [key]: value }, () => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError.message));
} else {
resolve();
}
});
});
}
// 读取数据
static async get(key, area = 'local') {
const storage = chrome.storage[area];
return new Promise((resolve, reject) => {
storage.get(key, (result) => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError.message));
} else {
resolve(typeof key === 'string' ? result[key] : result);
}
});
});
}
// 删除数据
static async remove(key, area = 'local') {
const storage = chrome.storage[area];
return new Promise((resolve) => {
storage.remove(key, resolve);
});
}
// 监听存储变化
static onChanged(callback) {
chrome.storage.onChanged.addListener((changes, areaName) => {
const formatted = {};
for (const [key, { oldValue, newValue }] of Object.entries(changes)) {
formatted[key] = { oldValue, newValue };
}
callback(formatted, areaName);
});
}
// 批量操作
static async batchSet(obj, area = 'local') {
const storage = chrome.storage[area];
return new Promise((resolve) => storage.set(obj, resolve));
}
}
// 使用示例
async function demo() {
await ExtensionStorage.set('theme', 'dark', 'sync');
await ExtensionStorage.set('fontSize', 14, 'sync');
await ExtensionStorage.set('cachedPages', {
'example.com': { html: '...', timestamp: Date.now() }
});
await ExtensionStorage.set('currentTask', { step: 2 }, 'session');
ExtensionStorage.onChanged((changes, area) => {
console.log(`[${area}] 数据变化:`, changes);
});
const theme = await ExtensionStorage.get('theme', 'sync');
console.log('当前主题:', theme);
}
六、Side Panel:MV3的杀手级特性
Side Panel(侧边栏)是Chrome 114引入的MV3专属功能,允许你在浏览器右侧显示一个持久化的面板。与Popup不同,Side Panel不会因为点击其他区域而关闭,非常适合做笔记工具、AI助手、实时数据分析等场景。
<!-- sidepanel.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, 'Segoe UI', sans-serif;
background: #f8f9fa; color: #333; padding: 16px;
}
.header {
display: flex; align-items: center; gap: 8px;
margin-bottom: 16px; padding-bottom: 12px;
border-bottom: 1px solid #e0e0e0;
}
.header h1 { font-size: 18px; font-weight: 600; }
.tabs { display: flex; gap: 4px; margin-bottom: 12px; }
.tab {
padding: 6px 16px; border: 1px solid #ddd;
border-radius: 16px; cursor: pointer; font-size: 13px;
background: white; transition: all 0.2s;
}
.tab.active { background: #4285f4; color: white; border-color: #4285f4; }
.card {
background: white; border-radius: 8px; padding: 12px;
margin-bottom: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.08);
}
.card h3 { font-size: 14px; margin-bottom: 6px; }
.card p { font-size: 13px; color: #666; line-height: 1.5; }
#content { min-height: 200px; }
.note-input {
width: 100%; min-height: 80px; padding: 10px;
border: 1px solid #ddd; border-radius: 8px;
font-size: 13px; resize: vertical; margin-bottom: 8px;
}
.btn {
padding: 8px 16px; border: none; border-radius: 6px;
background: #4285f4; color: white; cursor: pointer;
font-size: 13px;
}
.btn:hover { background: #3367d6; }
</style>
</head>
<body>
<div class="header">
<span>🔧</span>
<h1>我的助手</h1>
</div>
<div class="tabs">
<div class="tab active" data-tab="info">页面信息</div>
<div class="tab" data-tab="notes">笔记</div>
<div class="tab" data-tab="settings">设置</div>
</div>
<div id="content"></div>
<script src="sidepanel.js"></script>
</body>
</html>
// sidepanel.js
const tabs = document.querySelectorAll('.tab');
const content = document.getElementById('content');
tabs.forEach(tab => {
tab.addEventListener('click', () => {
tabs.forEach(t => t.classList.remove('active'));
tab.classList.add('active');
renderTab(tab.dataset.tab);
});
});
async function renderTab(tabName) {
switch (tabName) {
case 'info': await renderPageInfo(); break;
case 'notes': await renderNotes(); break;
case 'settings': await renderSettings(); break;
}
}
async function renderPageInfo() {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
const { tabHistory = [] } = await chrome.storage.local.get('tabHistory');
const currentHistory = tabHistory.filter(h => h.url === tab.url);
content.innerHTML = `
当前页面
标题: ${tab.title}
URL: ${tab.url}
访问历史 (${currentHistory.length}次)
${currentHistory.slice(-5).map(h => `
${new Date(h.timestamp).toLocaleString('zh-CN')}
`).join('')}
`;
}
async function renderNotes() {
const { notes = [] } = await chrome.storage.local.get('notes');
content.innerHTML = `
${notes.slice().reverse().map(n => `
${n.text}
${new Date(n.time).toLocaleString('zh-CN')}
`).join('')}
`;
document.getElementById('saveNote').addEventListener('click', async () => {
const text = document.getElementById('noteText').value.trim();
if (!text) return;
notes.push({ text, time: Date.now() });
await chrome.storage.local.set({ notes });
renderNotes();
});
}
async function renderSettings() {
const { settings } = await chrome.storage.local.get('settings');
content.innerHTML = `
插件设置
主题: ${settings?.theme || 'light'}
通知: ${settings?.enableNotifications ? '开启' : '关闭'}
`;
}
renderTab('info');
七、Popup面板开发
Popup是用户点击插件图标时弹出的小窗口。它与Side Panel的关键区别是:Popup会因失去焦点而关闭,适合做快速操作面板。
<!-- popup.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<style>
body { width: 350px; padding: 16px; font-family: sans-serif; }
.search-box {
width: 100%; padding: 10px; border: 2px solid #e0e0e0;
border-radius: 8px; font-size: 14px; outline: none;
transition: border-color 0.2s;
}
.search-box:focus { border-color: #4285f4; }
.actions { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 12px; }
.action-btn {
padding: 10px; border: 1px solid #e0e0e0; border-radius: 8px;
background: white; cursor: pointer; text-align: center;
transition: all 0.2s;
}
.action-btn:hover { background: #f0f4ff; border-color: #4285f4; }
.action-btn .icon { font-size: 20px; }
.action-btn .label { font-size: 12px; margin-top: 4px; color: #666; }
#result { margin-top: 12px; }
</style>
</head>
<body>
<input class="search-box" id="search" placeholder="搜索当前页面...">
<div class="actions">
<div class="action-btn" id="btnInfo">
<div class="icon">📊</div>
<div class="label">页面信息</div>
</div>
<div class="action-btn" id="btnSidePanel">
<div class="icon">📌</div>
<div class="label">打开侧栏</div>
</div>
<div class="action-btn" id="btnScreenshot">
<div class="icon">📸</div>
<div class="label">截图</div>
</div>
<div class="action-btn" id="btnSettings">
<div class="icon">⚙️</div>
<div class="label">设置</div>
</div>
</div>
<div id="result"></div>
<script src="popup.js"></script>
</body>
</html>
// popup.js
document.getElementById('search').addEventListener('input', async (e) => {
const keyword = e.target.value.trim();
if (!keyword) return;
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
chrome.tabs.sendMessage(tab.id, {
type: 'HIGHLIGHT_TEXT',
payload: { keyword }
});
});
document.getElementById('btnSidePanel').addEventListener('click', async () => {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
await chrome.sidePanel.open({ tabId: tab.id });
window.close();
});
document.getElementById('btnScreenshot').addEventListener('click', async () => {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
const dataUrl = await chrome.tabs.captureVisibleTab(null, { format: 'png' });
chrome.downloads.download({
url: dataUrl,
filename: `screenshot-${Date.now()}.png`
});
window.close();
});
八、调试技巧与常见问题
Chrome插件的调试比普通Web开发复杂一些,因为你需要同时调试多个上下文:
8.1 调试不同组件
- Service Worker:在 chrome://extensions/ 找到你的插件,点击”Service Worker”链接,打开DevTools
- Popup:右键点击插件图标 → “检查弹出内容”
- Content Script:在网页的DevTools中,Sources面板左侧会出现”Content scripts”分类
- Side Panel:在Side Panel打开时,右键 → “检查”
8.2 常见报错与解决
| 错误信息 | 原因 | 解决方案 |
|---|---|---|
| Unchecked runtime.lastError: Could not establish connection | Content Script未加载就发送消息 | 发送前检查tab是否完成加载,或使用try-catch |
| The message port closed before a response was received | 消息处理函数没有返回true | 异步消息处理需要return true保持通道开放 |
| Service worker registration failed | Service Worker代码有语法错误 | 检查background.js的语法,查看Service Worker DevTools的Console |
| Permission ‘xxx’ is not recognized | manifest.json中权限名拼写错误 | 检查Chrome官方文档的权限列表 |
| Content Security Policy violation | 在页面中执行了内联脚本 | MV3禁止内联脚本,所有JS必须是独立文件 |
8.3 热重载开发
开发时每次修改代码都需要手动点击刷新按钮重新加载插件,效率很低。推荐使用web-ext工具实现自动重载:
# 安装web-ext
npm install -g web-ext
# 启动开发模式(自动重载 + 自动打开Chrome)
web-ext run --source-dir ./my-extension
# 指定Chrome路径
web-ext run --source-dir ./my-extension --chromium-binary /usr/bin/google-chrome
# 只重载不打开新窗口
web-ext run --source-dir ./my-extension --no-input --reload
九、打包发布到Chrome Web Store
开发完成后,需要将插件发布到Chrome Web Store供用户安装:
# 1. 打包为zip(排除不需要的文件)
cd my-extension
zip -r ../my-extension-v1.0.0.zip . -x ".git/*" "node_modules/*" "*.md" "tests/*"
# 2. 使用web-ext打包(推荐,会自动验证)
web-ext build --source-dir ./my-extension --artifacts-dir ./dist
# 3. 验证包内容
web-ext lint --source-dir ./my-extension
发布流程:
- 访问 Chrome Web Store Developer Dashboard
- 支付一次性注册费 $5(个人开发者账号)
- 点击”New Item”上传zip文件
- 填写插件描述、截图(至少1张1280×800或640×400)、隐私政策
- 设置可见性(Public/Unlisted)
- 提交审核(通常需要几小时到3天)
审核常见被拒原因:权限请求过多且未说明用途、隐私政策缺失、描述与实际功能不符、使用了远程代码执行。建议只申请最小必要权限,并在描述中清楚说明每个权限的使用场景。
十、实战:构建一个网页摘要AI助手
最后,我们用一个实际项目串联所有知识。这个插件可以提取当前网页的核心内容,并通过API发送给AI进行摘要:
// content_script_extractor.js — 提取页面核心内容
function extractArticleContent() {
let article = document.querySelector('article');
if (!article) {
article = document.querySelector(
'[role="main"], .post-content, .article-body, .entry-content, #content'
);
}
if (!article) article = document.body;
const text = article.innerText
.replace(/\n{3,}/g, '\n\n')
.trim();
return {
title: document.title,
url: window.location.href,
text: text.substring(0, 8000),
wordCount: text.split(/\s+/).length,
extractedAt: new Date().toISOString()
};
}
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
if (msg.type === 'EXTRACT_CONTENT') {
const content = extractArticleContent();
sendResponse(content);
}
return true;
});
// background.js中增加摘要处理逻辑
async function summarizePage(tabId) {
// 1. 从content script提取内容
const pageContent = await chrome.tabs.sendMessage(tabId, {
type: 'EXTRACT_CONTENT'
});
// 2. 调用AI API生成摘要
const { settings } = await chrome.storage.local.get('settings');
const response = await fetch(`${settings.apiEndpoint}/summarize`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${settings.apiKey}`
},
body: JSON.stringify({
text: pageContent.text,
title: pageContent.title,
language: 'zh-CN'
})
});
const { summary, keywords } = await response.json();
// 3. 存储摘要结果
const { summaries = {} } = await chrome.storage.local.get('summaries');
summaries[pageContent.url] = {
title: pageContent.title,
summary,
keywords,
createdAt: Date.now()
};
await chrome.storage.local.set({ summaries });
// 4. 发送通知
chrome.notifications.create({
type: 'basic',
iconUrl: 'icons/icon128.png',
title: '摘要生成完成',
message: `已为「${pageContent.title}」生成摘要,点击侧栏查看`
});
return summary;
}
总结
Chrome插件Manifest V3的开发模式与传统Web开发有显著不同,核心要点回顾:
- Service Worker是事件驱动的,不要依赖全局状态,使用chrome.storage持久化数据
- Content Script运行在隔离沙箱中,通过消息API与Service Worker通信
- Side Panel提供了比Popup更好的持久化面板体验,适合复杂交互场景
- 使用web-ext工具链进行开发和打包,提高开发效率
- 遵循最小权限原则,只申请必要的API权限
Chrome插件开发的门槛不高,但要做一个好用、安全、高性能的插件,需要深入理解MV3的架构设计和各种边界情况。希望本文能帮助你快速上手并避开常见的坑。
汤不热吧