浏览器插件开发-service worker
因为popup的生命周期很短,所以我们需要编写一个background.js作为后台运行的脚本 如何配置 在manifest.json增加一个字段 "background": { "serv

浏览器插件开发-service worker

发布时间:2025-12-27 (2025-12-27)

因为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 才能拦截/修改
// );