SharedWorker 是唯一支持跨标签页共享状态的 Web Worker,必须在其中建立 WebSocket/MQTT 连接并管理重连、心跳与端口生命周期,页面仅发送指令;MQTT.js 需用浏览器版并通过 importScripts 加载,且需校验 client.connected 再订阅。

SharedWorker 是唯一能跨标签页共享状态的 Web Worker
浏览器里只有 SharedWorker 支持被多个浏览上下文(同源的窗口、iframe、标签页)同时连接,且共用同一全局执行环境。普通 Worker 每开一个标签页就新建一个实例,ServiceWorker 虽然全局,但不支持直接通信和长连接维护——它只响应 fetch/push 等事件,不能主动保持 WebSocket 或 MQTT 连接。
所以如果你要「一个连接、多端复用」,SharedWorker 不是可选项,是唯一可行路径。
连接必须在 SharedWorker 内部建立,不能由页面传入 socket 实例
页面无法把 WebSocket 或 MQTT.js 的 client 实例传给 SharedWorker:这些对象不可序列化,postMessage 会抛 DataCloneError。所有连接逻辑(包括重连、心跳、订阅管理)必须在 worker 内完成。
- 页面只负责发指令,比如
{ type: 'connect', url: 'wss://...' }或{ type: 'subscribe', topic: 'sensor/+/temp' } -
SharedWorker收到后自行调用new WebSocket()或mqtt.connect(),并缓存连接实例 - 连接失败时,worker 要自己做指数退避重连,不能依赖页面触发
- 所有消息广播需通过
port.postMessage()推给每个已连接的页面端口
MQTT.js 在 SharedWorker 中需用浏览器版 + 手动适配
mqtt.js 默认导出的是 Node.js 版本,直接 import mqtt from 'mqtt' 会在 SharedWorker 报错(依赖 net、fs)。必须显式使用浏览器构建版本,并注意以下几点:
- 从
node_modules/mqtt/dist/mqtt.min.js加载(CDN 可用https://unpkg.com/mqtt@4.14.5/dist/mqtt.min.js) - 用
self.importScripts()在 worker 全局加载,而非import(ESM 在 Worker 中支持有限,尤其旧 Chrome) - 创建 client 时必须传
{ protocols: ['mqtt', 'mqtts'] },否则某些代理(如 EMQX 的 WSS 端点)握手失败 - 不要用
client.on('connect')后立刻subscribe——MQTT 协议要求 CONNECT ACK 后才能发 SUBSCRIBE,mqtt.js内部已处理,但手动调用client.subscribe()必须等client.connected === true,否则静默丢弃
页面端口生命周期管理稍不注意就会断连或内存泄漏
每个页面通过 new SharedWorker('./worker.js') 获取 port,再 port.start() 开始通信。但页面关闭、刷新、崩溃时,这个 port 不会自动关闭,SharedWorker 里也不会触发 port.onclose ——除非你主动监听 port.onmessage 并约定心跳协议。
- 建议页面每 10s 发一次
{ type: 'ping' },worker 收到后更新该 port 的最后活跃时间戳 - worker 启动定时器,每 30s 扫描所有 port,对超时(比如 >60s 无 ping)的调用
port.close() - worker 自身不能监听
beforeunload(它没 window),但可以靠页面发送{ type: 'disconnect' }主动注销 - 若页面异常终止(如 kill tab),worker 最多延迟 1~2 分钟才感知不到 ping,这期间残留 port 不影响连接,但会浪费引用 —— 关键是别在 worker 里用
Map存大量未清理的 port 引用
最麻烦的不是连上,是连上之后谁来管“谁还在听”。这点容易被忽略,直到用户开 20 个标签页,worker 内存涨到几百 MB 才发现 port 积压没清理。

















