前言
上一篇我们介绍了SharedWorker,今天要介绍一种与SharedWorker 的"页面存活依赖"不同,即便在所有页面关闭后仍可后台运行,凭借"后台常驻"特性,实现跨页面、跨会话的通讯。它就是ServiceWorker。
本文就带你了解下ServiceWorker ,看看是如何进行跨页面通讯。
1. ServiceWorker 是什么?
在聊通讯之前,我们先了解ServiceWorker的核心定位------它是一种独立于页面主线程的后台线程,由浏览器管理,具备以下关键特性:
- 独立线程:运行在与页面完全隔离的线程中,不阻塞页面渲染,可执行网络请求、缓存管理等操作。
- 后台常驻:注册成功后会在后台持续运行,即使所有关联页面关闭,仍能响应事件(如推送通知、网络请求)。
- 同源限制:仅能控制与其注册页面同源的页面,且协议必须为HTTPS(本地开发可使用localhost例外)。
- 事件驱动 :通过监听
install、activate、message等事件实现功能逻辑,无DOM操作能力。
这些特性也是其实现跨页面通讯的基础,简单来说,ServiceWorker就像一个"驻留在浏览器中的微型服务端",多个页面可通过它建立通讯连接,实现数据共享与消息传递,甚至在页面离线时完成特定交互。
2. ServiceWorker 是如何进行跨页面通讯
2.1 ServiceWorker 生命周期
scss
注册 (Register)
↓
安装 (Install)
↓
激活 (Activate)
↓
运行 (Active)
↓
终止 (Terminated)

ServiceWorker的跨页面通讯核心是"中心化消息枢纽"模式,依托其"单例运行+多页面连接管理"的特性实现,具体流程可分为三个阶段:
- 注册与激活 :首个页面通过
navigator.serviceWorker.register()注册ServiceWorker脚本,浏览器启动后台线程并执行脚本,触发install和activate事件,此时ServiceWorker进入激活状态,具备通讯能力。 - 页面连接建立 :每个页面在ServiceWorker激活后,可通过
navigator.serviceWorker.controller获取激活的实例,或监听controllerchange事件确认连接,进而通过postMessage()建立消息通道。 - 消息分发与传递 :ServiceWorker通过监听
message事件接收任意页面的消息,可直接处理后反馈给发送页面,或通过clients.matchAll()获取所有连接的页面客户端,实现消息广播或点对点推送。
2.2 整体通讯架构
2.2.1 核心生命周期流程

2.2.2 关键流程详解
1. 页面连接注册流程

2. 消息路由分发流程

3. 广播与点对点消息流程
广播消息 :页面发送消息后,ServiceWorker向所有连接的客户端推送;点对点消息:精准定位目标页面ID,仅向指定客户端发送。
广播:

点对点:

4. 数据结构设计
使用Map存储客户端连接信息,确保页面ID与客户端实例的快速映射,支持高效的增删改查操作。
python
┌─────────────────────────────────────────────────────────────────┐
│ connections: Map<string, ClientInfo> │
│ ────────────────────────────────────── │
│ │
│ 结构示例: │
│ ┌───────────┬──────────────────────────────┐ │
│ │ Key │ Value │ │
│ ├───────────┼──────────────────────────────┤ │
│ │ 'page-123'│ { │ │
│ │ │ client: Client对象, │ │
│ │ │ id: 'client-abc123', │ │
│ │ │ pageId: 'page-123', │ │
│ │ │ lastActive: 1699999999999 │ │
│ │ │ } │ │
│ ├───────────┼──────────────────────────────┤ │
│ │ 'page-456'│ { │ │
│ │ │ client: Client对象, │ │
│ │ │ id: 'client-xyz789', │ │
│ │ │ pageId: 'page-456', │ │
│ │ │ lastActive: 1699999999999 │ │
│ │ │ } │ │
│ └───────────┴──────────────────────────────┘ │
│ │
│ 核心作用:保存client.id便于通过clients.matchAll()快速匹配目标 │
│ │
└─────────────────────────────────────────────────────────────────┘
3. 实践案例
ServiceWorker的通讯实现需区分"ServiceWorker脚本"(后台逻辑)和"页面脚本"(前端交互)两部分。
实现需求:同一域名下的pageA、pageB两个页面,通过ServiceWorker实现"点对点消息"和"全局广播消息"两种通讯模式。
3.1 步骤1:编写ServiceWorker核心脚本(sw.js)
该脚本负责监听页面连接、接收消息并实现分发逻辑,核心是通过clients对象管理所有连接的页面客户端。
整合注册管理、消息分发、连接清理等核心功能,支持注册、广播、点对点、心跳检测。
js
// Service Worker 跨页面通信脚本
// 存储所有连接的页面客户端(使用 Map,key 是 pageId,value 是 client)
const connections = new Map();
console.log('Service Worker 已加载');
// 安装事件
self.addEventListener('install', (event) => {
console.log('Service Worker 安装中...');
// 跳过等待,立即激活
self.skipWaiting();
});
// 激活事件
self.addEventListener('activate', (event) => {
console.log('Service Worker 已激活');
// 立即控制所有页面
event.waitUntil(self.clients.claim());
});
// 监听来自页面的消息
self.addEventListener('message', async (event) => {
console.log('Service Worker 收到消息:', event.data);
const { type, pageId, target, data } = event.data;
const client = event.source;
// 0. 处理注册消息
if (type === 'register') {
// 保存客户端连接
connections.set(pageId, {
client: client,
id: client.id,
pageId: pageId
});
console.log(`页面 ${pageId} 已注册,当前在线:[${Array.from(connections.keys()).join(', ')}]`);
// 发送注册成功消息
client.postMessage({
type: 'registered',
from: 'ServiceWorker',
data: `注册成功,当前在线 ${connections.size} 个页面`
});
return;
}
// 1. 处理广播消息
if (type === 'broadcast') {
console.log(`广播消息给 ${connections.size} 个连接`);
// 获取所有客户端(包括未注册的)
const allClients = await self.clients.matchAll({
type: 'window',
includeUncontrolled: true
});
// 遍历所有客户端发送消息
allClients.forEach((client) => {
try {
client.postMessage({
type: 'broadcast',
from: 'ServiceWorker',
sender: pageId,
data: `广播消息:${data}`
});
} catch (e) {
console.error('发送失败:', e);
}
});
}
// 2. 处理点对点消息
if (type === 'private') {
console.log(`私发消息给 ${target},当前连接:[${Array.from(connections.keys()).join(', ')}]`);
if (connections.has(target)) {
const targetConn = connections.get(target);
try {
// 通过 client ID 查找目标客户端
const allClients = await self.clients.matchAll({
type: 'window',
includeUncontrolled: true
});
const targetClient = allClients.find(c => c.id === targetConn.id);
if (targetClient) {
targetClient.postMessage({
type: 'private',
from: 'ServiceWorker',
sender: pageId,
data: `私发消息:${data}`
});
} else {
console.log(`警告:客户端 ${target} 已断开`);
connections.delete(target);
}
} catch (e) {
console.error('发送失败:', e);
connections.delete(target);
}
} else {
console.log(`警告:未找到目标页面 ${target}`);
}
}
// 3. 处理心跳检测(用于清理断开的连接)
if (type === 'heartbeat') {
// 更新最后活跃时间
if (connections.has(pageId)) {
const conn = connections.get(pageId);
conn.lastActive = Date.now();
connections.set(pageId, conn);
}
}
// 4. 处理断开连接
if (type === 'disconnect') {
connections.delete(pageId);
console.log(`页面 ${pageId} 断开连接,当前在线:[${Array.from(connections.keys()).join(', ')}]`);
}
// 5. 获取在线列表
if (type === 'get-online') {
client.postMessage({
type: 'online-list',
from: 'ServiceWorker',
data: Array.from(connections.keys())
});
}
});
// 定期清理断开的连接(每30秒检查一次)
setInterval(async () => {
const allClients = await self.clients.matchAll({
type: 'window',
includeUncontrolled: true
});
const activeClientIds = new Set(allClients.map(c => c.id));
// 清理已断开的连接
for (const [pageId, conn] of connections.entries()) {
if (!activeClientIds.has(conn.id)) {
console.log(`清理断开的连接: ${pageId}`);
connections.delete(pageId);
}
}
}, 30000);
3.2 步骤2:编写页面代码
pagesA页面支持ServiceWorker注册、广播消息、点对点通讯。
js
<body>
<h3>页面 A(标识:page-123)</h3>
<div id="status" class="status inactive">Service Worker 未激活</div>
<div>
<input type="text" id="msgInput" placeholder="输入消息">
<br>
<button onclick="sendBroadcast()">广播消息</button>
<button onclick="sendToPageB()">发给页面B</button>
<button onclick="getOnlineList()">获取在线列表</button>
</div>
<div id="log"></div>
<script>
// 页面唯一标识
const pageId = 'page-123';
let serviceWorkerReady = false;
// 日志输出函数
function addLog(message) {
const log = document.getElementById('log');
const time = new Date().toLocaleTimeString();
log.innerHTML += `<p>[${time}] ${message}</p>`;
log.scrollTop = log.scrollHeight;
}
// 更新状态
function updateStatus(active) {
const statusEl = document.getElementById('status');
if (active) {
statusEl.textContent = 'Service Worker 已激活';
statusEl.className = 'status active';
serviceWorkerReady = true;
} else {
statusEl.textContent = 'Service Worker 未激活';
statusEl.className = 'status inactive';
serviceWorkerReady = false;
}
}
// 注册 Service Worker
async function registerServiceWorker() {
if ('serviceWorker' in navigator) {
try {
const registration = await navigator.serviceWorker.register('./service-worker.js');
console.log('Service Worker 注册成功:', registration);
addLog('Service Worker 注册成功');
// 等待 Service Worker 激活
await navigator.serviceWorker.ready;
updateStatus(true);
addLog('Service Worker 已激活');
// 发送注册消息
navigator.serviceWorker.controller.postMessage({
type: 'register',
pageId: pageId
});
} catch (error) {
console.error('Service Worker 注册失败:', error);
addLog('Service Worker 注册失败: ' + error.message);
}
} else {
addLog('浏览器不支持 Service Worker');
}
}
// 监听来自 Service Worker 的消息
navigator.serviceWorker.addEventListener('message', (event) => {
console.log('页面A收到消息:', event.data);
const { type, from, sender, data } = event.data;
if (type === 'registered') {
addLog(`✓ ${data}`);
} else if (type === 'broadcast') {
addLog(`📢 ${sender ? '来自 ' + sender + ': ' : ''}${data}`);
} else if (type === 'private') {
addLog(`📨 ${sender ? '来自 ' + sender + ': ' : ''}${data}`);
} else if (type === 'online-list') {
addLog(`👥 在线列表: [${data.join(', ')}]`);
} else {
addLog(`收到:${JSON.stringify(event.data)}`);
}
});
// 发送广播消息
function sendBroadcast() {
if (!serviceWorkerReady) {
addLog('⚠ Service Worker 未就绪');
return;
}
const input = document.getElementById('msgInput');
if (!input.value.trim()) {
addLog('⚠ 请输入消息内容');
return;
}
navigator.serviceWorker.controller.postMessage({
type: 'broadcast',
pageId: pageId,
data: input.value
});
addLog(`📤 发送广播: ${input.value}`);
input.value = '';
}
// 发送点对点消息给页面B
function sendToPageB() {
if (!serviceWorkerReady) {
addLog('⚠ Service Worker 未就绪');
return;
}
const input = document.getElementById('msgInput');
if (!input.value.trim()) {
addLog('⚠ 请输入消息内容');
return;
}
navigator.serviceWorker.controller.postMessage({
type: 'private',
pageId: pageId,
target: 'page-456',
data: input.value
});
addLog(`📤 发送私信给 page-456: ${input.value}`);
input.value = '';
}
// 获取在线列表
function getOnlineList() {
if (!serviceWorkerReady) {
addLog('⚠ Service Worker 未就绪');
return;
}
navigator.serviceWorker.controller.postMessage({
type: 'get-online',
pageId: pageId
});
}
// 页面关闭时发送断开连接消息
window.addEventListener('beforeunload', () => {
if (serviceWorkerReady && navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage({
type: 'disconnect',
pageId: pageId
});
}
});
// 初始化
registerServiceWorker();
// 定期发送心跳(可选,用于检测连接状态)
setInterval(() => {
if (serviceWorkerReady && navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage({
type: 'heartbeat',
pageId: pageId
});
}
}, 10000);
</script>
</body>
3.3 步骤3:页面B
页面B与页面A结构相似,仅需修改页面标识和目标页面ID即可,核心适配点:
php
// 1. 修改页面唯一标识为page-456
const pageId = 'page-456';
// 2. 调整发送私信按钮的目标页面ID
function sendToPageB() {
// ...(逻辑与sendToPageA一致)
navigator.serviceWorker.controller.postMessage({
type: 'private',
pageId: pageId,
target: 'page-123', // 目标为页面B的标识
data: content
});
}
3.4 步骤4:运行与调试说明
- 环境准备:将sw.js和页面文件放在同一目录,通过HTTP服务启动(如live-server、http-server),本地开发可直接用localhost访问,避免file://协议问题。
- 调试工具 :在Chrome浏览器中直接访问地址:
chrome://inspect/#workers;页面会列出当前浏览器中所有运行的Worker实例,找到目标ServiceWorker对应的"inspect"链接并点击,即可打开专属控制台。
3. 功能验证:同时打开页面A和页面B,点击"广播消息"可看到双方均收到;页面A发送"发给页面B"则仅页面B收到消息,实现点对点通讯。

4. ServiceWorker注意事项
4.1 协议与作用域限制(最常见坑)
ServiceWorker仅支持HTTPS协议(localhost和127.0.0.1为开发例外),若在HTTP环境下使用会直接报错。同时,其"作用域(scope)"决定了可控制的页面范围:
- 默认scope为注册脚本所在目录,例如在
/js/sw.js注册,默认仅控制/js/目录下的页面。 - 若需控制整个网站,需将sw.js放在根目录,或注册时指定
scope: '/',且服务器需配置Service-Worker-Allowed: /响应头。
4.2 脚本更新机制复杂,易导致通讯异常
ServiceWorker注册后会缓存脚本,若修改sw.js后直接刷新页面,新脚本不会立即生效,需通过以下方式触发更新:
- 页面中调用
registration.update()主动检查更新。 - 修改sw.js的文件内容(哪怕是注释),浏览器会检测到文件哈希变化,触发
install事件。 - 更新后需通过
self.skipWaiting()和clients.claim()让新脚本立即接管所有页面,否则需关闭所有页面后重新打开才生效。
4.3 消息数据序列化限制
通过postMessage()传递的消息数据需支持"结构化克隆算法",无法传递函数、DOM元素、Blob等复杂类型。解决方案:
- 简单数据:直接传递对象或数组。
- 复杂数据:将
Blob转为ArrayBuffer,将函数通过JSON.stringify序列化(需确保无循环引用)。
4.4 客户端管理需处理异常场景
ServiceWorker通过clients.matchAll()获取页面客户端时,需注意:
- 部分页面可能处于"冻结状态"(如后台标签页),需通过
client.focus()激活后再发送消息。 - 客户端实例可能失效,发送消息前需通过
client.url或client.id验证有效性,避免报错。
4.5 兼容性与降级处理
ServiceWorker在IE浏览器中完全不支持,Safari在iOS 11.3以上才支持。实际项目中需做好降级:
javascript
// 降级处理示例:不支持时使用localStorage+storage事件替代
if (!navigator.serviceWorker) {
log('浏览器不支持ServiceWorker,启用localStorage降级方案');
// 监听localStorage变化实现跨页面通讯
window.addEventListener('storage', (e) => {
if (e.key === 'userLoginState') {
const state = JSON.parse(e.newValue);
updateLoginStatus(state);
}
});
}
5. 总结:ServiceWorker 通讯的最佳实践
最后总结一下:ServiceWorker是前端在需要离线支持、跨会话同步及后台协同场景下的最优通讯方案,使用时需根据自己的适用场景使用。
如有错误,请指正O^O!