当前位置: 首页 > java >正文

Chrome插件开发【Service Worker练手小项目】

目录

前言

项目一:智能定时页面刷新器

项目初始化

项目结构

manifest.json

popup界面实现

popup.html

popup.js

popup.css

Service Worker实现

基础定时逻辑

消息通信处理

BUG

项目二:跨标签通信中心

项目初始化

项目结构

manifest.json

popup界面实现

popup.html

popup.js

popup.css

content-script

其它


前言

在前几篇文章中,我们初步了解了Chromemanifestservice worker,下面让我们通过两个小项目来强化学习吧!

PS:在项目中会涉及到未学过的API,具体API可以参考官网文档(只需要了解参数组成即可),后面会详细讲解所有API

项目一:智能定时页面刷新器

项目初始化

项目结构

/smart-timer-refresher├── icons/│   └── logo.png│   │   ├── popup/│   ├── popup.html│   ├── popup.js│   └── popup.css│├── background.js└── manifest.json

manifest.json

{"manifest_version":3,"name":"智能页面刷新器","version":"1.0","description":"一款可以自动刷新页面的小插件","icons":{"16":"icons/logo.png","48":"icons/logo.png","128":"icons/logo.png",},"action":{"default_popup":"popup.html"},"background":{"service_worker":"background.js","type":"module"},"permissions":["alarms", "tabs", "storage"],"host_permissions":["<all_urls>"]
}

popup界面实现

popup.html

<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>欢迎使用简单扩展</title><link rel="stylesheet" href="popup.css">
</head>
<body><div class="control-group"><label for="toggle">启用自动刷新</label><input type="checkbox" id="toggle"></div><div class="control-group"><label for="interval">刷新间隔 (分钟)</label><input type="number" id="interval" min="1" max="120"></div><div id="status" class="inactive">状态: 未启用</div><script src="popup.js"></script>
</body>
</html>

popup.js

document.addEventListener('DOMContentLoaded',async () => {// 获取当前设置const { enabled,interval = 5 } = await chrome.storage.sync.get(['enabled','interval'])// 初始化UI状态const toggle = document.getElementById('toggle')const intervalInput = document.getElementById('interval')const statusDiv = document.getElementById('status')toggle.checked = enabledintervalInput.value = intervalupdateStatus(enabled)// 事件监听toggle.addEventListener('change',async (e) => {const enabled = e.target.checkedawait chrome.runtime.sendMessage({action:'toggle',value:enabled})updateStatus(enabled)})intervalInput.addEventListener('change',async (e) => {const value = parseInt(e.target.value)if (value >= 1 && value <= 120){await chrome.runtime.sendMessage({action:'setInterval',value})}})// 更新状态显示function updateStatus(enabled){statusDiv.textContent = `状态:${enabled ? '启用' : '禁用'}`statusDiv.className = enabled ? 'active' : 'inactive'}
})

popup.css

body {width: 250px;padding: 15px;font-family: Arial;
}
.control-group {margin-bottom: 15px;
}
label {display: block;margin-bottom: 5px;font-weight: bold;
}
input[type="number"] {width: 50px;padding: 5px;
}
#status {margin-top: 10px;padding: 5px;border-radius: 3px;
}
.active {background: #4CAF50;color: white;
}
.inactive {background: #f44336;color: white;
}

Service Worker实现

基础定时逻辑

// 初始化
chrome.runtime.onInstalled.addListener(async () => {chrome.storage.sync.get(['enabled','interval'],(res) => {if (res.enabled === undefined){chrome.storage.sync.set({enabled: true,interval: 5})}})managerAlarm(1)
})// 启动/停止计时器
function managerAlarm(interval){chrome.alarms.clear('autoRefresh');if (interval > 0){chrome.alarms.create('autoRefresh',{delayInMinutes:0.1,periodInMinutes:interval})}
}// 定时任务处理
chrome.alarms.onAlarm.addListener(async (alarm) => {if (alarm.name !== 'autoRefresh'){return;}const { enabled } = await chrome.storage.sync.get(['enabled'])if (!enabled){return;}const tabs = await chrome.tabs.query({active:true,// currentWindow:true 在onAlarm的上下文中,没有currentWindow这个概念,添加后查询为空数组})if (!tabs.length){return;}const currentTab = tabs[0]chrome.tabs.reload(currentTab.id)console.log(`当前标签页${currentTab.url}已刷新`)
})

消息通信处理

// 接收来自popup的控制命令
chrome.runtime.onMessage.addListener((request,sender,sendResponse) => {switch (request.action) {case 'toggle':chrome.storage.sync.set({enabled:request.value},() => {if (request.value){chrome.storage.sync.get('interval',(res) => {managerAlarm(request.interval || 5)})}else {chrome.alarms.clear('autoRefresh')}sendResponse({success:true})})return truecase 'setInterval':chrome.storage.sync.set({interval:request.value},() => {chrome.storage.sync.get('enabled',(res) => {if (res.enabled){managerAlarm(request.value)}sendResponse({success:true})})})return true}
})

效果:

Chrome DevTools工具中也可以查询到:

BUG

  • onAlarm事件中,使用tabs.query无法带currentWindow参数,因为onAlarm上下文中没有currentWindow这个概念,带这个参数就会查询到空数组

项目二:跨标签通信中心

项目初始化

项目结构

/cross-tab-communication├── icons/│   └── logo.png│   │   ├── popup/│   ├── popup.html│   ├── popup.css│   └── popup.js├── content.js│   ├── background.js└── manifest.json

manifest.json

{"manifest_version":3,"name":"跨标签通讯中心","version":"1.0","description":"实现不同标签页之间的消息传递","icons":{"16":"icons/logo.png","48":"icons/logo.png","128":"icons/logo.png"},"action":{"default_popup":"popup/popup.html"},"background":{"service_worker":"background.js","type":"module"},"content_scripts":[{"matches":["<all_urls>"],"js":["content.js"],"run_at":"document_end"}],"permissions":["scripting", "tabs", "storage"],"host_permissions":["<all_urls>"]
}

popup界面实现

popup.html

<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>通讯控制中心</title><link rel="stylesheet" href="popup.css">
</head>
<body><div class="container"><h1>标签页通讯中心</h1><div class="section"><h2>活动标签页</h2><div id="activeTabs" class="tab-list"></div></div><div class="section"><h2>发送消息</h2><select id="messageType"><option value="broadcast">广播</option><option value="direct">定向发送</option><option value="group">群组发送</option></select><div id="targetContainer" style="display: none;"><label for="targetTab">目标标签页:</label><select id="targetTab"></select></div><div id="groupContainer" style="display: none;"><label for="groupSelect">群组:</label><select id="groupSelect"></select></div><textarea id="messageInput" placeholder="输入消息内容"></textarea><button id="sendButton">发送</button></div><div class="section"><h2>群组管理</h2><input type="text" id="groupName" placeholder="新群组名"><button id="createGroup">创建群组</button><div id="groupsList"></div></div></div><script src="popup.js"></script>
</body>
</html>

popup.js

document.addEventListener('DOMContentLoaded', async () => {// 获取当前所有标签页const tabs = await chrome.tabs.query({});renderTabList(tabs);// 消息类型切换document.getElementById('messageType').addEventListener('change', (e) => {document.getElementById('targetContainer').style.display = e.target.value === 'direct' ? 'block' : 'none';document.getElementById('groupContainer').style.display = e.target.value === 'group' ? 'block' : 'none';});// 发送消息document.getElementById('sendButton').addEventListener('click', () => {const type = document.getElementById('messageType').value;const content = document.getElementById('messageInput').value;let message = { type, content };if (type === 'direct') {message.targetTabId = parseInt(document.getElementById('targetTab').value);} else if (type === 'group') {message.groupId = document.getElementById('groupSelect').value;}chrome.runtime.sendMessage(message);});// 创建群组document.getElementById('createGroup').addEventListener('click', async () => {const groupName = document.getElementById('groupName').value;if (!groupName) return;const { groups = {} } = await chrome.storage.local.get('groups');const groupId = `group_${Date.now()}`;await chrome.storage.local.set({groups: {...groups,[groupId]: {name: groupName,members: []}}});renderGroups();});// 初始渲染renderGroups();
});// 渲染标签页列表
function renderTabList(tabs) {const container = document.getElementById('activeTabs');const targetSelect = document.getElementById('targetTab');container.innerHTML = '';targetSelect.innerHTML = '';tabs.forEach(tab => {// 活动标签页列表const tabEl = document.createElement('div');tabEl.className = 'tab-item';tabEl.innerHTML = `<span>${tab.title || '无标题'}</span><small>${new URL(tab.url).hostname}</small><button data-tabid="${tab.id}" class="add-to-group">+群组</button>`;container.appendChild(tabEl);// 目标选择下拉框const option = document.createElement('option');option.value = tab.id;option.textContent = `${tab.title} (${tab.url})`;targetSelect.appendChild(option);});// 添加群组按钮事件document.querySelectorAll('.add-to-group').forEach(btn => {btn.addEventListener('click', async (e) => {const tabId = parseInt(e.target.dataset.tabid);const groupId = prompt('输入要添加到的群组ID');if (groupId) {const { groups = {} } = await chrome.storage.local.get('groups');if (groups[groupId]) {groups[groupId].members.push(tabId);await chrome.storage.local.set({ groups });renderGroups();} else {alert('群组不存在');}}});});
}// 渲染群组列表
async function renderGroups() {const { groups = {} } = await chrome.storage.local.get('groups');const container = document.getElementById('groupsList');const groupSelect = document.getElementById('groupSelect');container.innerHTML = '';groupSelect.innerHTML = '';Object.entries(groups).forEach(([id, group]) => {// 群组列表const groupEl = document.createElement('div');groupEl.className = 'group-item';groupEl.innerHTML = `<h3>${group.name} (${group.members.length}成员)</h3><button data-groupid="${id}" class="send-to-group">发送消息</button><button data-groupid="${id}" class="remove-group">删除</button>`;container.appendChild(groupEl);// 群组选择下拉框const option = document.createElement('option');option.value = id;option.textContent = group.name;groupSelect.appendChild(option);});// 绑定群组按钮事件document.querySelectorAll('.send-to-group').forEach(btn => {btn.addEventListener('click', () => {document.getElementById('messageType').value = 'group';document.getElementById('groupSelect').value = btn.dataset.groupid;document.getElementById('groupContainer').style.display = 'block';document.getElementById('targetContainer').style.display = 'none';});});document.querySelectorAll('.remove-group').forEach(btn => {btn.addEventListener('click', async () => {const { groups = {} } = await chrome.storage.local.get('groups');delete groups[btn.dataset.groupid];await chrome.storage.local.set({ groups });renderGroups();});});
}

popup.css

body {width: 400px;padding: 15px;font-family: Arial, sans-serif;margin: 0;
}.container {display: flex;flex-direction: column;gap: 20px;
}.section {border: 1px solid #ddd;padding: 15px;border-radius: 5px;
}.tab-list, .groups-list {display: flex;flex-direction: column;gap: 10px;max-height: 200px;overflow-y: auto;
}.tab-item, .group-item {padding: 10px;border: 1px solid #eee;border-radius: 4px;display: flex;justify-content: space-between;align-items: center;
}button {padding: 5px 10px;background: #4CAF50;color: white;border: none;border-radius: 3px;cursor: pointer;
}button:hover {opacity: 0.8;
}textarea {width: 100%;min-height: 80px;margin: 10px 0;padding: 8px;box-sizing: border-box;
}select, input[type="text"] {width: 100%;padding: 8px;margin: 5px 0 15px;box-sizing: border-box;
}

content-script

// 连接到后台
const port = chrome.runtime.connect({ name: 'tab-port' });// 当前标签页信息
const tabId = chrome.devtools?.inspectedWindow?.tabId || (new URLSearchParams(window.location.search)).get('tabId') || Math.random().toString(36).substr(2, 9);// 监听后台消息
port.onMessage.addListener((message) => {console.log('收到后台消息:', message);// 在实际标签页中显示消息if (message.type === 'show-notification') {showNotification(message.content);}
});// 发送消息到后台
function sendToBackground(message) {port.postMessage({...message,tabId: tabId,url: window.location.href});
}// 在页面上显示通知
function showNotification(text) {const div = document.createElement('div');div.style.position = 'fixed';div.style.bottom = '20px';div.style.right = '20px';div.style.padding = '10px';div.style.background = '#4CAF50';div.style.color = 'white';div.style.borderRadius = '5px';div.style.zIndex = '9999';div.textContent = text;document.body.appendChild(div);setTimeout(() => div.remove(), 3000);
}// 暴露接口给网页
window.CrossTabComm = {send: sendToBackground,onMessage: (callback) => {port.onMessage.addListener(callback);}
};// 初始化完成通知
sendToBackground({ type: 'tab-connected' });

其它

更多“Chrome插件开发”,可以参考我的专栏:

Chrome插件_是洋洋a的博客-CSDN博客

http://www.xdnf.cn/news/17608.html

相关文章:

  • 【LeetCode刷题集】--排序(三)
  • 【智能的起源】人类如何模仿,简单的“刺激-反应”机制 智能的核心不是记忆,而是发现规律并能迁移到新场景。 最原始的智能:没有思考,只有简单条件反射
  • Mamba 原理汇总2
  • AI(2)-神经网络(激活函数)
  • 支持小语种的在线客服系统,自动翻译双方语言,适合对接跨境海外客户
  • 数据结构-数组扩容
  • 开发指南130-实体类的主键生成策略
  • Apache ECharts 6 核心技术解密 – Vue3企业级可视化实战指南
  • 排错000
  • 基于 ZooKeeper 的分布式锁实现原理是什么?
  • windows上RabbitMQ 启动时报错:发生系统错误 1067。 进程意外终止。
  • 150V降压芯片DCDC150V100V80V降压12V5V1.5A车载仪表恒压驱动H6203L惠洋科技
  • git:分支
  • 提示词工程实战:用角色扮演让AI输出更专业、更精准的内容
  • 软件测评中HTTP 安全头的配置与测试规范
  • 数据变而界面僵:Vue/React/Angular渲染失效解析与修复指南
  • 基于 Axios 的 HTTP 请求封装文件解析
  • Console Variables Editor插件使用
  • 音视频学习(五十三):音频重采样
  • QT QProcess + xcopy 实现文件拷贝
  • Web安全自动化测试实战指南:Python与Selenium在验证码处理中的应用
  • Mybatis @Param参数传递说明
  • 【工作笔记】Wrappers.lambdaQuery()用法
  • RK3588在YOLO12(seg/pose/obb)推理任务中的加速方法
  • JS数组排序算法
  • 打靶日常-upload-labs(21关)
  • 【密码学】8. 密码协议
  • Android 开发问题:Invalid id; ID definitions must be of the form @+id/ name
  • 【系统分析师】软件需求工程——第11章学习笔记(上)
  • A#语言详解