因为popup的生命周期很短,所以我们需要编写一个background.js作为后台运行的脚本
如何配置
在manifest.json增加一个字段
"background": {
"service_worker": "background.js"
},
- 它是一个无界面、轻量级的后台脚本(对应
background.js),独立于 popup、网页标签页运行; - 它没有 DOM 访问权限(不能用
document/window),也不能操作页面元素; - 它是事件驱动的:只有触发特定事件(如扩展安装、消息接收、定时器)时才会激活,闲置一段时间(约 30 秒)后会自动休眠,事件触发时又会唤醒;
如果要看backgroud.js的日志,在插件页面点检查视图即可

核心能做这些事:
- 监听 / 响应扩展生命周期事件(安装、更新、启动);
- 处理扩展内组件通信(popup ↔ background ↔ content script);
- 执行后台任务(定时器、网络请求、数据存储);
- 监听 / 操作浏览器资源(标签页、书签、下载、通知);
- 管理扩展权限和状态。
生命周期
| 方法 / 事件 | 触发时机 |
|---|---|
install |
Service Worker 首次安装 / 更新时触发(仅一次),可用于缓存静态资源 |
activate |
Service Worker 安装完成后激活触发,可用于清理旧缓存、初始化状态 |
fetch |
扩展内发起网络请求时触发(可拦截、修改请求 / 响应) |
message |
接收来自内容脚本 / 弹窗页面的消息时触发 |
runtime.onStartup |
浏览器启动时触发(若扩展已启用) |
runtime.onInstalled |
扩展首次安装、更新或浏览器更新后首次加载时触发(比 install 更通用) |
runtime.onMessage |
等同于 message,是 Manifest V3 更推荐的消息监听方式 |
self.clients.claim() |
激活后调用,让 Service Worker 立即控制所有已打开的扩展相关页面 |
service worker通信
与popup通信
- SW 向 popup 发消息时,仅当 popup 处于打开状态才能收到;
- 如果 popup 已关闭,消息会丢失(可通过
chrome.storage暂存消息,popup 打开时读取)。
sw使用chrome.runtime.sendMessage发消息,popup用chrome.runtime.onMessage.addListener接收消息
与content script通信
SW 和 Content Script 的通信依赖标签页 ID(因为 Content Script 绑定到具体标签页)
sw用chrome.tabs.sendMessage(tabId, message)(指定标签页发送)
右键菜单
"permissions": [
"contextMenus", // 必须声明右键菜单权限
"activeTab", // 可选,用于操作当前标签页
"storage" // 可选,用于保存菜单状态
],
// 定义菜单ID常量(便于管理)
const MENU_IDS = {
MAIN_MENU: 'main_menu', // 主菜单
SUB_MENU: 'sub_menu', // 子菜单
SELECTION_MENU: 'selection_menu' // 仅选中文本时显示的菜单
};
// ========== 1. 插件安装/更新时创建右键菜单 ==========
chrome.runtime.onInstalled.addListener(async (details) => {
console.log('插件安装/更新:', details.reason);
// 先清空旧菜单(避免重复创建)
chrome.contextMenus.removeAll();
// 创建主菜单
chrome.contextMenus.create({
id: MENU_IDS.MAIN_MENU,
title: '🔧 我的插件功能', // 菜单显示的文字
contexts: ['all'] // 触发场景:all(所有场景)、page(页面空白处)、selection(选中文本)等
});
// 创建子菜单(依赖主菜单)
chrome.contextMenus.create({
id: MENU_IDS.SUB_MENU,
title: '📝 打印当前页面URL',
parentId: MENU_IDS.MAIN_MENU, // 指定父菜单ID,成为子菜单
contexts: ['all']
});
// 创建仅选中文本时显示的菜单
chrome.contextMenus.create({
id: MENU_IDS.SELECTION_MENU,
title: '🔍 搜索选中的文本:%s', // %s会自动替换为选中的文本
contexts: ['selection'] // 仅选中文本时显示
});
console.log('右键菜单创建完成');
});
// ========== 2. 监听右键菜单的点击事件 ==========
chrome.contextMenus.onClicked.addListener(async (info, tab) => {
console.log('右键菜单被点击:', { info, tab });
// 根据菜单ID处理不同逻辑
switch (info.menuItemId) {
case MENU_IDS.SUB_MENU:
// 处理“打印当前页面URL”逻辑
if (tab?.id) {
console.log(`当前页面URL:${tab.url}`);
// 也可以给Content Script发消息,在页面内显示
chrome.tabs.sendMessage(tab.id, {
type: 'SHOW_URL',
url: tab.url
}).catch(err => console.error('发送消息失败:', err));
}
break;
case MENU_IDS.SELECTION_MENU:
// 处理“搜索选中的文本”逻辑
const selectedText = info.selectionText;
if (selectedText) {
// 打开新标签页搜索文本(比如用百度)
chrome.tabs.create({
url: `https://www.baidu.com/s?wd=${encodeURIComponent(selectedText)}`,
active: true
});
}
break;
default:
console.log('未知菜单ID:', info.menuItemId);
}
});
// ========== 3. 进阶:动态更新右键菜单(示例) ==========
// 比如根据存储状态修改菜单标题
async function updateMenuTitle() {
const { enabled } = await chrome.storage.sync.get({ enabled: true });
const newTitle = enabled ? '🔧 我的插件功能(已启用)' : '🔧 我的插件功能(已禁用)';
chrome.contextMenus.update(MENU_IDS.MAIN_MENU, {
title: newTitle
}, () => {
if (chrome.runtime.lastError) {
console.error('更新菜单失败:', chrome.runtime.lastError);
} else {
console.log('菜单标题更新成功');
}
});
}
// 监听存储变化,动态更新菜单
chrome.storage.onChanged.addListener((changes) => {
if (changes.enabled) {
updateMenuTitle();
}
});
// ========== 4. 进阶:删除指定菜单(示例) ==========
// 比如插件禁用时删除菜单
async function removeMenu() {
chrome.contextMenus.remove(MENU_IDS.SELECTION_MENU, () => {
if (chrome.runtime.lastError) {
console.error('删除菜单失败:', chrome.runtime.lastError);
} else {
console.log('选中文本菜单已删除');
}
});
}
// 测试:5秒后删除选中文本菜单(仅演示)
// setTimeout(removeMenu, 5000);
网络请求
如果需要发网络请求的话
需要在minifest.json声明
"host_permissions": ["<all_urls>"],
监听网络请求
// 监听请求发起阶段(beforeSendHeaders),可修改请求头
chrome.webRequest.onBeforeSendHeaders.addListener(
(details) => {
console.log("【请求发起】", {
url: details.url, // 请求URL
method: details.method, // 请求方法(GET/POST等)
tabId: details.tabId, // 所属标签页ID
requestHeaders: details.requestHeaders // 请求头
});
// 示例:修改请求头(需声明 webRequestBlocking 权限)
// details.requestHeaders.push({ name: "Custom-Header", value: "MyExtension" });
// return { requestHeaders: details.requestHeaders };
},
{
urls: ["<all_urls>"], // 监听所有URL,与清单文件保持一致
types: ["main_frame", "sub_frame", "script", "image", "xmlhttprequest"] // 监听的请求类型
},
["requestHeaders"] // 需要获取的请求信息(如需修改,需添加 "blocking")
);
// 监听响应接收阶段(onCompleted),获取响应信息
chrome.webRequest.onCompleted.addListener(
(details) => {
console.log("【请求完成】", {
url: details.url,
statusCode: details.statusCode, // HTTP状态码
responseHeaders: details.responseHeaders, // 响应头
response: details
});
},
{ urls: ["<all_urls>"] },
["responseHeaders"]
);
// 监听请求错误(onErrorOccurred)
chrome.webRequest.onErrorOccurred.addListener(
(details) => {
console.error("【请求错误】", {
url: details.url,
error: details.error // 错误信息
});
},
{ urls: ["<all_urls>"] }
);
// 可选:拦截并取消特定请求(需添加 webRequestBlocking 权限)
// chrome.webRequest.onBeforeRequest.addListener(
// (details) => {
// // 拦截所有png图片请求
// if (details.url.endsWith(".png")) {
// return { cancel: true };
// }
// },
// { urls: ["<all_urls>"], types: ["image"] },
// ["blocking"] // 必须添加 blocking 才能拦截/修改
// );