SharedWorker
SharedWorker 是一种特殊的 Web Worker,可以被多个浏览器标签页(或 iframe)共享。与普通 Worker(Dedicated Worker)每个页面独占一个实例不同,SharedWorker 在同源的多个标签页之间只存在一个实例,所有标签页通过独立的端口(Port)与之通信。本节介绍 SharedWorker 的创建、端口通信机制和典型应用场景。
前置知识
阅读本节前,建议先了解:Worker 通信
基础概念
SharedWorker vs Dedicated Worker
| 特性 | Dedicated Worker | SharedWorker |
|---|---|---|
| 实例数 | 每次创建一个新实例 | 同源共享一个实例 |
| 通信方式 | worker.onmessage | 通过 port.postMessage |
| 标签页共享 | 不支持 | 支持 |
| 适用场景 | 单页计算 | 跨标签页数据共享 |
SharedWorker 的优势
- 节省资源:多个标签页共享一个 Worker,减少内存占用
- 数据共享:所有标签页可以共享状态和数据
- 集中管理:统一的计算逻辑和数据源
浏览器支持
| 浏览器 | 支持 | 备注 |
|---|---|---|
| Chrome | 支持 | 所有版本 |
| Firefox | 支持 | 不支持 importScripts 在 Worker 中 |
| Safari | 不支持 | -- |
| Edge | 支持 | Chromium 内核 |
语法与创建
创建 SharedWorker
javascript
// 创建 SharedWorker
const sharedWorker = new SharedWorker('shared-worker.js');
// 获取通信端口
const port = sharedWorker.port;
// 通过端口发送消息
port.start(); // 必须调用 start() 来激活端口
port.postMessage('Hello SharedWorker');
// 通过端口接收消息
port.onmessage = function(event) {
console.log('收到 SharedWorker 的消息:', event.data);
};SharedWorker 脚本 (shared-worker.js)
javascript
// shared-worker.js
const connections = []; // 存储所有连接的端口
// 当新的标签页连接时
self.onconnect = function(event) {
const port = event.ports[0]; // 获取连接端口
connections.push(port);
const clientId = connections.length;
console.log(`标签页 ${clientId} 已连接,当前连接数: ${connections.length}`);
// 监听该端口的消息
port.onmessage = function(e) {
console.log(`来自标签页 ${clientId}:`, e.data);
// 处理消息并回复
const response = `已处理: ${e.data}`;
port.postMessage(response);
// 可选:广播给所有其他标签页
connections.forEach((conn, i) => {
if (i !== clientId - 1) {
conn.postMessage(`标签页 ${clientId} 发送了: ${e.data}`);
}
});
};
// 向新连接的标签页发送欢迎消息
port.postMessage(`欢迎!你是第 ${clientId} 个连接的标签页`);
};详细说明
端口通信详解
javascript
// 主线程
const worker = new SharedWorker('shared-worker.js');
const port = worker.port;
// 必须调用 start() 激活端口(如果使用 onmessage)
port.start();
// 发送消息
port.postMessage({
type: 'chat',
message: '大家好'
});
// 接收消息
port.onmessage = (event) => {
console.log(event.data);
};
// 关闭连接
port.close();javascript
// Worker 端
self.onconnect = (event) => {
const port = event.ports[0];
// Worker 端也需要 start()
port.start();
port.onmessage = (event) => {
// 处理消息
port.postMessage({ reply: '收到' });
};
// 监听端口关闭
port.onclose = () => {
console.log('标签页断开了连接');
const index = connections.indexOf(port);
if (index > -1) connections.splice(index, 1);
};
};SharedWorker 连接管理
javascript
// shared-worker.js - 带连接管理的 SharedWorker
const ports = new Map();
let nextId = 0;
self.onconnect = (event) => {
const port = event.ports[0];
const id = ++nextId;
ports.set(id, { port, metadata: {} });
port.onmessage = (e) => {
const { action, payload } = e.data;
switch (action) {
case 'register':
// 注册标签页信息
ports.get(id).metadata = payload;
broadcast({
type: 'user-list',
users: Array.from(ports.values()).map(p => p.metadata)
});
break;
case 'message':
// 转发消息给所有标签页
broadcast({
type: 'message',
from: ports.get(id).metadata.name,
content: payload
});
break;
case 'get-count':
port.postMessage({
type: 'count',
count: ports.size
});
break;
}
};
port.onclose = () => {
ports.delete(id);
broadcast({ type: 'user-list', users: Array.from(ports.values()).map(p => p.metadata) });
};
// 发送连接成功确认
port.postMessage({ type: 'connected', id });
};
// 广播消息给所有连接
function broadcast(message) {
ports.forEach(({ port }) => {
port.postMessage(message);
});
}实战示例
跨标签页共享状态
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>SharedWorker 跨标签页通信</title>
<style>
body { font-family: -apple-system, sans-serif; padding: 20px; }
.card {
max-width: 600px; margin: 0 auto; padding: 24px;
background: white; border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
h2 { text-align: center; }
.info {
padding: 12px; background: #e3f2fd; border-radius: 8px;
margin-bottom: 16px; font-size: 14px; color: #1565c0;
}
.input-row {
display: flex; gap: 10px; margin-bottom: 16px;
}
.input-row input {
flex: 1; padding: 10px; border: 2px solid #e0e0e0;
border-radius: 8px; font-size: 14px;
}
.btn {
padding: 10px 20px; border: none; border-radius: 8px;
cursor: pointer; font-size: 14px; font-weight: 600;
color: white; background: #1a73e8;
}
.counter {
display: flex; gap: 10px; margin-bottom: 16px;
}
.counter .btn { flex: 1; text-align: center; }
.counter-value {
font-size: 48px; font-weight: 700; text-align: center;
color: #1a73e8; margin: 16px 0;
}
.chat-log {
height: 300px; overflow-y: auto; padding: 12px;
background: #1a1a2e; border-radius: 8px;
font-family: monospace; font-size: 13px; color: #0f0;
}
</style>
</head>
<body>
<div class="card">
<h2>SharedWorker 跨标签页通信</h2>
<div class="info">
请打开多个标签页查看效果。共享计数器和聊天消息在所有标签页间同步。
</div>
<h3>共享计数器</h3>
<div class="counter">
<button class="btn" onclick="changeCount(-1)">-</button>
<div class="counter-value" id="counterValue">0</div>
<button class="btn" onclick="changeCount(1)">+</button>
</div>
<h3>跨标签页聊天</h3>
<div class="input-row">
<input type="text" id="chatInput" placeholder="输入消息,按回车发送" onkeydown="if(event.key==='Enter')sendChat()">
<button class="btn" onclick="sendChat()">发送</button>
</div>
<div class="chat-log" id="chatLog">等待连接...</div>
</div>
<script>
// 创建 SharedWorker
const workerCode = `
let count = 0;
const ports = new Map();
let nextId = 0;
self.onconnect = (event) => {
const port = event.ports[0];
const id = ++nextId;
ports.set(id, port);
port.start();
port.postMessage({ type: 'init', count, tabId: id });
port.onmessage = (e) => {
const { type, value } = e.data;
if (type === 'count-change') {
count += value;
ports.forEach(p => p.postMessage({ type: 'count-update', count }));
}
if (type === 'chat') {
ports.forEach(p => p.postMessage({ type: 'chat', from: id, value, time: Date.now() }));
}
};
port.onclose = () => ports.delete(id);
};
`;
const worker = new SharedWorker(
URL.createObjectURL(new Blob([workerCode], { type: 'application/javascript' }))
);
const port = worker.port;
port.start();
let myTabId = null;
port.onmessage = (event) => {
const { type, count, tabId, value, time } = event.data;
if (type === 'init') {
myTabId = tabId;
document.getElementById('counterValue').textContent = count;
addLog(`已连接(标签页 #${tabId})`);
}
if (type === 'count-update') {
document.getElementById('counterValue').textContent = count;
}
if (type === 'chat') {
const prefix = tabId === myTabId ? '我' : `标签页#${tabId}`;
addLog(`[${prefix}] ${value}`);
}
};
function changeCount(delta) {
port.postMessage({ type: 'count-change', value: delta });
}
function sendChat() {
const input = document.getElementById('chatInput');
const msg = input.value.trim();
if (msg) {
port.postMessage({ type: 'chat', value: msg });
input.value = '';
}
}
function addLog(text) {
const log = document.getElementById('chatLog');
const time = new Date().toLocaleTimeString();
log.innerHTML += `<p>[${time}] ${text}</p>`;
log.scrollTop = log.scrollHeight;
}
</script>
</body>
</html>注意事项
- Safari 不支持 SharedWorker:需要提供降级方案
- 端口必须调用
start():使用onmessage时两端都需要调用 - 页面关闭时端口自动断开:但 Worker 实例不会销毁(直到最后一个连接关闭)
- 调试困难:SharedWorker 的 console.log 不会显示在任何标签页的控制台
最佳实践
- 使用 SharedWorker 实现跨标签页的状态共享
- 维护连接列表,及时清理断开的端口
- 为每个连接分配唯一 ID,便于消息追踪
- 在 SharedWorker 中集中管理共享数据
下一节
继续学习:Service Worker