SharedWorker

深入理解 SharedWorker:与 Web Worker 的区别、端口通信、多标签页状态共享、以及实际应用场景。

SharedWorker

一、SharedWorker 概述

1.1 什么是 SharedWorker

SharedWorker 是一种特殊的 Web Worker,可以被同源的不同窗口、标签页、iframe 同时使用。与普通 Web Worker(专用)不同,SharedWorker 可以被多个浏览上下文共享。

// 创建 SharedWorker
const worker = new SharedWorker('/js/shared-worker.js');

// 通过端口通信
worker.port.addEventListener('message', (e) => {
  console.log('Received:', e.data);
});

worker.port.start();
worker.port.postMessage('Hello from main thread');

1.2 与 Web Worker 的区别

特性 Web Worker SharedWorker
作用域 仅创建它的脚本 同源所有脚本共享
多实例 每次 new 创建新实例 同脚本同一实例
通信方式 直接 postMessage 通过 port.postMessage
状态共享 独立状态 可共享状态
标签页通信 需要通过主线程中转 可直接互相通信

二、SharedWorker 实现

2.1 Worker 脚本

// shared-worker.js
const connections = new Set();
let sharedState = { count: 0 };

self.onconnect = (e) => {
  // 为每个连接创建端口
  const port = e.ports[0];

  // 添加到连接集合
  connections.add(port);

  // 发送当前状态
  port.postMessage({ type: 'init', state: sharedState });

  // 监听消息
  port.onmessage = (event) => {
    const { type, data } = event.data;

    if (type === 'increment') {
      sharedState.count += 1;
      // 广播到所有连接
      connections.forEach(p => {
        p.postMessage({
          type: 'update',
          state: sharedState
        });
      });
    }
  };

  // 启动端口
  port.start();

  // 连接关闭时移除
  port.onmessage = null;
  port.onmessageerror = null;
  connections.delete(port);
};

2.2 客户端代码

// main.js
const worker = new SharedWorker('/js/shared-worker.js');

worker.port.addEventListener('message', (e) => {
  const { type, state } = e.data;

  if (type === 'init') {
    console.log('Initial state:', state);
  } else if (type === 'update') {
    console.log('Updated state:', state);
    updateUI(state);
  }
});

worker.port.start();

// 发送消息
document.getElementById('btn').addEventListener('click', () => {
  worker.port.postMessage({ type: 'increment' });
});

三、端口通信

3.1 端口基础

// 创建时自动获得 port
const sharedWorker = new SharedWorker('worker.js');

// 显式获取端口
const port = sharedWorker.port;

// 启动端口
port.start();

// 发送消息
port.postMessage({ type: 'ping' });

// 接收消息
port.onmessage = (e) => {
  console.log('Message:', e.data);
};

3.2 多个标签页通信

// SharedWorker 内部
self.onconnect = (e) => {
  const port = e.ports[0];

  port.onmessage = (event) => {
    // 接收来自任意标签页的消息
    const { from, message } = event.data;

    // 广播给所有其他标签页
    connections.forEach(p => {
      if (p !== port) {
        p.postMessage({
          from: 'another-tab',
          message: message
        });
      }
    });
  };

  port.start();
};

3.3 错误处理

port.onmessageerror = (e) => {
  console.error('Message error:', e);
};

// SharedWorker 内部错误
self.onerror = (e) => {
  console.error('Worker error:', e.message, e.filename, e.lineno);
};

四、实际应用

4.1 跨标签页状态同步

// SharedWorker: state-manager.js
const state = {
  user: null,
  settings: {},
  cache: {}
};

const subscribers = new Set();

self.onconnect = (e) => {
  const port = e.ports[0];
  subscribers.add(port);

  // 发送初始状态
  port.postMessage({ type: 'init', state });

  port.onmessage = (event) => {
    const { type, payload } = event.data;

    switch (type) {
      case 'update':
        Object.assign(state, payload);
        // 广播更新
        subscribers.forEach(p => {
          p.postMessage({ type: 'update', state });
        });
        break;

      case 'subscribe':
        // 发送完整状态
        port.postMessage({ type: 'init', state });
        break;
    }
  };

  port.start();
};

4.2 连接池管理

// shared-worker.js - 连接池
const connections = [];
const MAX_CONNECTIONS = 10;

self.onconnect = (e) => {
  if (connections.length >= MAX_CONNECTIONS) {
    e.ports[0].postMessage({
      type: 'error',
      message: 'Connection pool full'
    });
    return;
  }

  const port = e.ports[0];
  connections.push(port);

  // ... 处理连接 ...

  port.onmessage = null;
  connections.splice(connections.indexOf(port), 1);
};

4.3 心跳检测

// shared-worker.js
const heartbeatInterval = 5000;
let lastHeartbeat = Date.now();

setInterval(() => {
  if (Date.now() - lastHeartbeat > heartbeatInterval * 2) {
    // 标签页可能已崩溃,清理僵尸连接
    cleanupZombieConnections();
  }
}, heartbeatInterval);

function cleanupZombieConnections() {
  connections.forEach(port => {
    port.postMessage({ type: 'ping' });
  });
}

五、注意事项

5.1 浏览器支持

// 检测支持
if ('SharedWorker' in window) {
  console.log('SharedWorker supported');
} else {
  console.warn('SharedWorker not supported');
}

5.2 HTTPS 要求

// SharedWorker 需要安全上下文
if (window.isSecureContext) {
  // SharedWorker 可用
} else {
  console.warn('SharedWorker requires HTTPS');
}

5.3 调试

// Chrome DevTools 中查看 SharedWorker
// 1. 打开 DevTools
// 2. 选择 "Sources" 面板
// 3. 在 "Threads" 或 "Workers" 中查看
// 4. 设置断点进行调试

六、面试高频问题

Q: SharedWorker 和 Web Worker 有什么区别?

回答要点:Web Worker 是专用的,只能被创建它的脚本使用;SharedWorker 可以被同源的多个脚本共享。这使得 SharedWorker 适合需要跨标签页共享状态或通信的场景,而 Web Worker 适合隔离的独立任务。

Q: SharedWorker 的端口通信机制是什么?

回答要点:SharedWorker 通过端口(port)与每个连接建立通信。onconnect 事件提供端口,通过 port.postMessage 发送消息,port.onmessage 接收消息。多个标签页通过各自的端口与 SharedWorker 通信,SharedWorker 可以广播消息给所有或部分端口。

Q: SharedWorker 适合什么应用场景?

回答要点:跨标签页的状态同步(如用户登录状态);跨标签页的数据库连接池;跨标签页的缓存管理;跨标签页的日志收集;需要长连接且需要在多标签页间共享的场景(如 WebSocket 连接池)。


参考资料

延展阅读