Skip to content

SharedWorker

SharedWorker 是一种特殊的 Web Worker,可以被多个浏览器标签页(或 iframe)共享。与普通 Worker(Dedicated Worker)每个页面独占一个实例不同,SharedWorker 在同源的多个标签页之间只存在一个实例,所有标签页通过独立的端口(Port)与之通信。本节介绍 SharedWorker 的创建、端口通信机制和典型应用场景。

前置知识

阅读本节前,建议先了解:Worker 通信

基础概念

SharedWorker vs Dedicated Worker

特性Dedicated WorkerSharedWorker
实例数每次创建一个新实例同源共享一个实例
通信方式worker.onmessage通过 port.postMessage
标签页共享不支持支持
适用场景单页计算跨标签页数据共享

SharedWorker 的优势

  1. 节省资源:多个标签页共享一个 Worker,减少内存占用
  2. 数据共享:所有标签页可以共享状态和数据
  3. 集中管理:统一的计算逻辑和数据源

浏览器支持

浏览器支持备注
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>

注意事项

  1. Safari 不支持 SharedWorker:需要提供降级方案
  2. 端口必须调用 start():使用 onmessage 时两端都需要调用
  3. 页面关闭时端口自动断开:但 Worker 实例不会销毁(直到最后一个连接关闭)
  4. 调试困难:SharedWorker 的 console.log 不会显示在任何标签页的控制台

最佳实践

  1. 使用 SharedWorker 实现跨标签页的状态共享
  2. 维护连接列表,及时清理断开的端口
  3. 为每个连接分配唯一 ID,便于消息追踪
  4. 在 SharedWorker 中集中管理共享数据

下一节

继续学习:Service Worker

参考链接