JavaScript中的共享工作者
本文介绍了JavaScript中的Shared Worker。
我们将一步一步地讲解从Shared Worker的基础到实际应用场景的所有内容。
YouTube Video
javascript-shared-worker.html
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8">
5 <title>JavaScript & HTML</title>
6 <style>
7 * {
8 box-sizing: border-box;
9 }
10
11 body {
12 margin: 0;
13 padding: 1em;
14 padding-bottom: 10em;
15 font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
16 background-color: #f7f9fc;
17 color: #333;
18 line-height: 1.6;
19 }
20
21 .container {
22 max-width: 800px;
23 margin: 0 auto;
24 padding: 1em;
25 background-color: #ffffff;
26 border: 1px solid #ccc;
27 border-radius: 10px;
28 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
29 }
30
31 .container-flex {
32 display: flex;
33 flex-wrap: wrap;
34 gap: 2em;
35 max-width: 1000px;
36 margin: 0 auto;
37 padding: 1em;
38 background-color: #ffffff;
39 border: 1px solid #ccc;
40 border-radius: 10px;
41 box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
42 }
43
44 .left-column, .right-column {
45 flex: 1 1 200px;
46 min-width: 200px;
47 }
48
49 h1, h2 {
50 font-size: 1.2rem;
51 color: #007bff;
52 margin-top: 0.5em;
53 margin-bottom: 0.5em;
54 border-left: 5px solid #007bff;
55 padding-left: 0.6em;
56 background-color: #e9f2ff;
57 }
58
59 button {
60 display: block;
61 margin: 1em auto;
62 padding: 0.75em 1.5em;
63 font-size: 1rem;
64 background-color: #007bff;
65 color: white;
66 border: none;
67 border-radius: 6px;
68 cursor: pointer;
69 transition: background-color 0.3s ease;
70 }
71
72 button:hover {
73 background-color: #0056b3;
74 }
75
76 #output {
77 margin-top: 1em;
78 background-color: #1e1e1e;
79 color: #0f0;
80 padding: 1em;
81 border-radius: 8px;
82 min-height: 200px;
83 font-family: Consolas, monospace;
84 font-size: 0.95rem;
85 overflow-y: auto;
86 white-space: pre-wrap;
87 }
88
89 .highlight {
90 outline: 3px solid #ffc107; /* yellow border */
91 background-color: #fff8e1; /* soft yellow background */
92 transition: background-color 0.3s ease, outline 0.3s ease;
93 }
94
95 .active {
96 background-color: #28a745; /* green background */
97 color: #fff;
98 box-shadow: 0 0 10px rgba(40, 167, 69, 0.5);
99 transition: background-color 0.3s ease, box-shadow 0.3s ease;
100 }
101 </style>
102</head>
103<body>
104 <div class="container">
105 <h1>JavaScript Console</h1>
106 <button id="executeBtn">Execute</button>
107 <div id="output"></div>
108 </div>
109
110 <script>
111 // Override console.log to display messages in the #output element
112 (function () {
113 // Override console.log
114 const originalLog = console.log;
115 console.log = function (...args) {
116 originalLog.apply(console, args);
117 const message = document.createElement('div');
118 message.textContent = args.map(String).join(' ');
119 output.appendChild(message);
120 };
121
122 // Override console.error
123 const originalError = console.error;
124 console.error = function (...args) {
125 originalError.apply(console, args);
126 const message = document.createElement('div');
127 message.textContent = args.map(String).join(' ');
128 message.style.color = 'red'; // Color error messages red
129 output.appendChild(message);
130 };
131 })();
132
133 document.getElementById('executeBtn').addEventListener('click', () => {
134 // Prevent multiple loads
135 if (document.getElementById('externalScript')) return;
136
137 const script = document.createElement('script');
138 script.src = 'javascript-shared-worker.js';
139 script.id = 'externalScript';
140 //script.onload = () => console.log('javascript-shared-worker.js loaded and executed.');
141 //script.onerror = () => console.log('Failed to load javascript-shared-worker.js.');
142 document.body.appendChild(script);
143 });
144 </script>
145</body>
146</html>JavaScript中的共享工作者
Shared Worker是什么?
Shared Worker是一种可以在同一源下被多个页面(标签页、窗口、iframe等)共享的工作线程。与特定页面的 Dedicated Worker 不同,主要特点是可以在多个页面之间共享同一个后台进程。典型的使用场景包括:。
- 你可以在多个标签页之间共享一个WebSocket连接,从而减少连接数量并集中管理重连。
- 你可以在标签页之间同步状态(如Pub/Sub或数据存储的集中管理)。
- 你可以序列化IndexedDB操作,以协调并发访问。
- 你可以防止高计算量过程的重复执行。
Shared Worker与Service Worker的作用不同。Service Worker主要充当网络代理,而Shared Worker侧重于跨页面执行任意计算或共享状态管理。
基本API与生命周期
创建(主线程)
1// main.js
2// The second argument 'name' serves as an identifier to share the same SharedWorker
3// if both the URL and name are the same.
4const worker = new SharedWorker('/shared-worker.js', { name: 'app-core' });
5
6// Get the communication port (MessagePort) with this SharedWorker
7const port = worker.port;
8
9// Receive messages with onmessage, send messages with postMessage
10port.onmessage = (ev) => {
11 console.log('From SharedWorker:', JSON.stringify(ev.data));
12};
13
14// When using onmessage, start() is not required
15// (start() is needed when using addEventListener instead)
16port.postMessage({ type: 'hello', from: location.pathname });- 这段代码演示了如何创建
Shared Worker并通过其端口收发消息。
接收端(在Shared Worker作用域内)
1// shared-worker.js
2// SharedWorkerGlobalScope has an onconnect event,
3// and a MessagePort is provided for each connection
4const ports = new Set();
5
6onconnect = (e) => {
7 const port = e.ports[0];
8 ports.add(port);
9
10 port.onmessage = (ev) => {
11 // Log the received message and send a reply back to the sender
12 console.log('From page:', ev.data);
13 port.postMessage({ type: 'ack', received: ev.data });
14 };
15
16 // For handling explicit disconnection from the page (via MessagePort.close())
17 port.onmessageerror = (err) => {
18 console.error('Message error:', err);
19 };
20
21 // Greeting immediately after connection
22 port.postMessage({ type: 'welcome', clientCount: ports.size });
23};
24
25// Explicit termination of the SharedWorker is usually unnecessary
26// (It will be garbage collected when all ports are closed)
- 此代码展示了如何在
Shared Worker内部为每一个连接接收MessagePort并处理、回复来自客户端的消息。
Shared Worker的生命周期
Shared Worker 会在建立第一个连接时启动,并可能在最后一个端口关闭时终止。当页面刷新/关闭时连接被关闭,如有需要会重新创建worker。
Shared Worker 是在浏览器中共享的"长生命周期脚本"。如果你不注意 Shared Worker 的生命周期,更容易出现资源泄漏、陈旧状态持久化和意外重启等问题。
试试看:标签页之间的广播
这是一个最简单的实现,多个标签页连接到同一个Shared Worker,任何发送的消息都会广播给所有标签页。
main.js
1const worker = new SharedWorker('/shared-worker.js', { name: 'bus' });
2const port = worker.port;
3
4port.onmessage = (ev) => {
5 const msg = ev.data;
6 if (msg.type === 'message') {
7 const li = document.createElement('li');
8 li.textContent = `[${new Date(msg.at).toLocaleTimeString()}] ${msg.payload}`;
9 document.querySelector('#messages').appendChild(li);
10 } else if (msg.type === 'ready') {
11 console.log(`Connected. clients=${msg.clients}`);
12 }
13};
14
15document.querySelector('#form').addEventListener('submit', (e) => {
16 e.preventDefault();
17 const input = document.querySelector('#text');
18 port.postMessage({ type: 'publish', payload: input.value });
19 input.value = '';
20});
21
22document.querySelector('#ping').addEventListener('click', () => {
23 port.postMessage({ type: 'ping' });
24});- 此代码连接到
Shared Worker,实现了消息的接收/显示、通过发送表单发送消息,以及点击按钮发送ping。
shared-worker.js
1// Minimal pub/sub bus in a SharedWorker
2const subscribers = new Set();
3
4/** Broadcast to all connected ports */
5function broadcast(message) {
6 for (const port of subscribers) {
7 try {
8 port.postMessage(message);
9 } catch (e) {
10 // Unsubscribe if sending fails, just in case
11 subscribers.delete(port);
12 }
13 }
14}
15
16onconnect = (e) => {
17 const port = e.ports[0];
18 subscribers.add(port);
19
20 port.onmessage = (ev) => {
21 const msg = ev.data;
22 if (msg && msg.type === 'publish') {
23 broadcast({ type: 'message', payload: msg.payload, at: Date.now() });
24 } else if (msg && msg.type === 'ping') {
25 port.postMessage({ type: 'pong', at: Date.now() });
26 }
27 };
28
29 port.postMessage({ type: 'ready', clients: subscribers.size });
30};- 此代码在
Shared Worker内部实现了简单的发布/订阅功能,在多个客户端之间转发消息。
index.html
1<!doctype html>
2<html>
3 <body>
4 <form id="form">
5 <input id="text" placeholder="say something" />
6 <button>Send</button>
7 </form>
8 <button id="ping">Ping</button>
9 <ul id="messages"></ul>
10 <script src="/main.js" type="module"></script>
11 </body>
12</html>- 如果你在多个标签页中打开此页面,从任一标签页发送的消息都能通知其他标签页。
消息设计:请求/响应和关联ID
当多个客户端与Shared Worker交互时,你经常需要识别哪个响应对应哪个请求。因此,标准做法是包含相关 ID 以将响应与请求关联起来。
main.js
1function createClient(workerUrl, name) {
2 const w = new SharedWorker(workerUrl, { name });
3 const port = w.port;
4 const pending = new Map(); // id -> {resolve,reject}
5 let nextId = 1;
6
7 port.onmessage = (ev) => {
8 const { id, ok, result, error } = ev.data || {};
9 if (!id || !pending.has(id)) return;
10 const { resolve, reject } = pending.get(id);
11 pending.delete(id);
12 ok ? resolve(result) : reject(new Error(error));
13 };
14
15 function call(method, params) {
16 const id = nextId++;
17 return new Promise((resolve, reject) => {
18 pending.set(id, { resolve, reject });
19 port.postMessage({ id, method, params });
20 });
21 }
22
23 return { call };
24}
25
26// usage
27const client = createClient('/shared-worker.js', 'rpc');
28client.call('add', { a: 2, b: 3 }).then(console.log); // -> 5
29client.call('sleep', { ms: 500 }).then(console.log); // -> 'done'
- 此代码实现了一个简单的RPC客户端,可以向
Shared Worker发起异步方法调用。 这里的 RPC 服务器(远程过程调用服务器)是指提供从其他程序或进程调用函数和过程机制的服务器。 - 在此示例中,
id只是简单递增,但你也可以使用 UUID、随机字符串,或将时间戳与计数器结合来创建唯一键。
shared-worker.js
1// A small request/response router with correlation ids
2onconnect = (e) => {
3 const port = e.ports[0];
4
5 port.onmessage = async (ev) => {
6 const { id, method, params } = ev.data || {};
7 try {
8 let result;
9 switch (method) {
10 case 'add':
11 result = (params?.a ?? 0) + (params?.b ?? 0);
12 break;
13 case 'sleep':
14 await new Promise(r => setTimeout(r, params?.ms ?? 0));
15 result = 'done';
16 break;
17 default:
18 throw new Error(`Unknown method: ${method}`);
19 }
20 port.postMessage({ id, ok: true, result });
21 } catch (err) {
22 port.postMessage({ id, ok: false, error: String(err) });
23 }
24 };
25};- 该代码实现了一个简单的 RPC 服务器,根据
method执行处理,并将结果与请求 ID 一起返回。
典型使用模式
使用Shared Worker时,有几种常见实用模式,如下:。
多路复用一个WebSocket(在标签页之间共享)
如果每个标签页都打开自己的 WebSocket 连接,会影响服务器的负载和连接数上限。将唯一一个WebSocket放在Shared Worker中,每个标签页通过worker收发消息。
main.js
1const w = new SharedWorker('/shared-worker.js', { name: 'ws' });
2const port = w.port;
3
4port.onmessage = (ev) => {
5 const msg = ev.data;
6 if (msg.type === 'data') {
7 console.log('Server says:', msg.payload);
8 } else if (msg.type === 'socket') {
9 console.log('WS state:', msg.state);
10 }
11};
12
13function send(payload) {
14 port.postMessage({ type: 'send', payload });
15}- 此代码实现了通过
Shared Worker向WebSocket服务器发送/接收消息和获得连接状态通知的流程。
shared-worker.js
1let ws;
2const ports = new Set();
3
4function ensureSocket() {
5 if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {
6 return;
7 }
8 ws = new WebSocket('wss://example.com/stream');
9 ws.onopen = () => broadcast({ type: 'socket', state: 'open' });
10 ws.onclose = () => broadcast({ type: 'socket', state: 'closed' });
11 ws.onerror = (e) => broadcast({ type: 'socket', state: 'error', detail: String(e) });
12 ws.onmessage = (m) => broadcast({ type: 'data', payload: m.data });
13}
14
15function broadcast(msg) {
16 for (const p of ports) p.postMessage(msg);
17}
18
19onconnect = (e) => {
20 const port = e.ports[0];
21 ports.add(port);
22 ensureSocket();
23
24 port.onmessage = (ev) => {
25 const msg = ev.data;
26 if (msg?.type === 'send' && ws?.readyState === WebSocket.OPEN) {
27 ws.send(JSON.stringify(msg.payload));
28 }
29 };
30};-
此代码在
Shared Worker中实现了共享单一WebSocket连接的机制,将连接状态和收到的数据广播给所有客户端,将每个客户端的请求通过WebSocket发送给服务器。 -
通过在
Shared Worker中集中处理重连策略和未发送消息队列,所有标签页的行为变得一致。
IndexedDB操作调度(序列化)
当从多个标签页访问同一个数据库时,为避免并发事务冲突或锁等待,可以在Shared Worker中将请求排队,顺序处理。
1// db-worker.js (very simplified – error handling omitted)
2let db;
3async function openDB() {
4 if (db) return db;
5 db = await new Promise((resolve, reject) => {
6 const req = indexedDB.open('appdb', 1);
7 req.onupgradeneeded = () => req.result.createObjectStore('kv');
8 req.onsuccess = () => resolve(req.result);
9 req.onerror = () => reject(req.error);
10 });
11 return db;
12}
13
14async function put(key, value) {
15 const d = await openDB();
16 return new Promise((resolve, reject) => {
17 const tx = d.transaction('kv', 'readwrite');
18 tx.objectStore('kv').put(value, key);
19 tx.oncomplete = () => resolve(true);
20 tx.onerror = () => reject(tx.error);
21 });
22}
23
24async function get(key) {
25 const d = await openDB();
26 return new Promise((resolve, reject) => {
27 const tx = d.transaction('kv', 'readonly');
28 const req = tx.objectStore('kv').get(key);
29 req.onsuccess = () => resolve(req.result);
30 req.onerror = () => reject(req.error);
31 });
32}
33
34onconnect = (e) => {
35 const port = e.ports[0];
36 port.onmessage = async (ev) => {
37 const { id, op, key, value } = ev.data;
38 try {
39 const result = op === 'put' ? await put(key, value) : await get(key);
40 port.postMessage({ id, ok: true, result });
41 } catch (err) {
42 port.postMessage({ id, ok: false, error: String(err) });
43 }
44 };
45};- 此代码实现了通过
Shared Worker对IndexedDB的读写请求进行排队,从而串行化多标签页的访问,避免冲突与锁竞争。
模块化与打包工具提示
如果你想将Shared Worker脚本写成ES模块,不同环境对其行为和支持可能不同。实际中,更安全的做法是选择以下之一:。
- 编写为传统worker格式,如需依赖通过
importScripts()加载。 - 使用打包工具的worker入口功能(如Vite / Webpack / esbuild等),在构建时为
Shared Worker单独打包。
错误处理、断线检测与健壮性建议
关于错误处理和健壮性,请考虑以下几点:。
-
处理连接建立前的发送 将端口未准备好时到达的消息进行排队。
-
断线检测
MessagePort没有标准的onclose事件。在主页面中,在beforeunload期间使用port.postMessage发送{type: 'bye'}消息,或在协议中明确指定,以确保 worker 被正确清理。 -
重连 当页面刷新或标签页重新打开时,会新建一个端口。准备一个初始同步消息(用于一次性发送完整状态)。
-
反压机制 在高频广播时,切换为节流/防抖或发送快照。
-
安全性
Shared Worker本质上只在同一个源内可共享。如果在worker中存放敏感信息,建议在调用方通过令牌等方式进行侧面验证和设计。
如何正确使用Dedicated Worker、Shared Worker和Service Worker
每种类型的Worker具有以下特点:。
-
Dedicated WorkerDedicated Worker只供单个页面使用。它实现了计算与UI的1:1分离。 -
Shared WorkerShared Worker可以被同一源下的多个页面共享。非常适合标签页间通信和共享同一个连接。 -
Service WorkerService Worker可用于网络代理、缓存、离线操作、推送通知和后台同步。其优势在于可以拦截fetch请求。
经验法则:如果需要在标签页间进行信息共享和任意处理,用
Shared Worker;如果需要网络控制,用Service Worker;如果只是想将大量计算从 UI 中分离出来,用Dedicated Worker。
常见陷阱
使用Shared Worker时,需要注意以下几点。
-
忘记调用
start()或不必要的调用 使用port.addEventListener('message', ...)时,必须调用port.start()。如果使用port.onmessage = ...则不需要这样做。 -
无条件广播 随着标签页数量的增加,负载也会增加。可通过实现差异化投递或订阅主题(根据主题过滤)来降低负载。
-
对象复制的成本
postMessage会复制数据。对于大数据,可以考虑以可转移对象(例如ArrayBuffer)发送,或结合Atomics使用共享内存(SharedArrayBuffer)。 -
生命周期与重新初始化 当最后一个客户端断开后,
Shared Worker可能会被终止。需要为首次连接和重启妥善设计初始化逻辑,以避免产生副作用和Bug。
总结
Shared Worker是实现多个页面共享的长寿命自定义逻辑的地方。- 明确消息协定(类型/协议),并通过添加关联ID使你的请求/响应设计更加健壮。
- 它适用于所有标签页统一处理时效果更佳的场景,例如复用WebSocket连接或串行化访问IndexedDB等。
- 对打包工具、类型定义和重连机制等细节进行优化,能极大提升可维护性和用户体验。
您可以在我们的YouTube频道上使用Visual Studio Code跟随上述文章进行学习。 请也查看我们的YouTube频道。