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 不同,Shared Worker 的主要特點是同一個背景程序可以被多個頁面共同使用。其典型的使用場景包括:。
- 您可以在多個分頁間共用一個 WebSocket 連線,減少連線數並集中管理重連。
- 您可以同步多個分頁間的狀態(集中管理發佈/訂閱或資料儲存)。
- 您可以序列化 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>- 如果您在多個分頁開啟本頁面,任何分頁傳送的訊息都會通知其他分頁。
訊息設計:請求/回應與關聯識別碼
多個客戶端與 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'
- 這段程式實作一個可對
Shared Worker進行非同步方法呼叫的簡易 RPC 客戶端。 這裡的 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};- 這段程式碼將 IndexedDB 的讀寫操作透過
Shared Worker排隊,使多分頁存取序列化,避免衝突和鎖競爭。
模組化與打包工具技巧
如果您要以 ES Module 撰寫 Shared Worker 腳本,各環境的行為與支援度會有所差異。實務上,建議採用下列做法之一較為保險:。
- 以經典 worker 格式撰寫,如有需要再用
importScripts()載入相依套件。 - 利用工具(如 Vite/ Webpack/ esbuild 等)啟用worker 入口功能,於建置時為
Shared Worker分別打包。
錯誤處理、斷線偵測與健壯性技巧
提高錯誤處理與健壯性時可留意以下幾點:。
-
連線建立前的訊息處理 將在連線埠尚未就緒前收到的訊息進行排隊。
-
偵測斷線
MessagePort沒有標準的onclose事件處理器。在主端於beforeunload時,利用port.postMessage發送一個{type: 'bye'}訊息,或者在協議中明確指定,以確保 worker 能被正確清理。 -
重新連線 頁面重新載入或分頁重開時會產生新的連線埠。準備一個初始同步訊息(一次傳遞完整狀態)。
-
回壓處理 大量廣播時可改用節流/防抖或只發送快照。
-
安全性
Shared Worker基本上只能在同一來源內共享。如果要在 worker 內放入機密資料,請考慮讓呼叫端透過 token 或類似方式進行端點驗證防護。
如何正確使用 Dedicated Worker、Shared Worker 與 Service Worker
每種 Worker 具有以下特點:。
-
Dedicated WorkerDedicated Worker僅限單一頁面使用。它能將計算任務與 UI 進行一對一的分離。 -
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會複製資料。當資料量大時,建議以 Transferable(如ArrayBuffer)傳送,或配合Atomics使用共享記憶體(SharedArrayBuffer)。 -
存續期間與重新初始化
Shared Worker會在最後一個客戶端離線時終止。請正確設計首次連線與重啟時的初始化流程,以避免副作用與錯誤。
總結
Shared Worker適合實作多頁面共用的長存自訂邏輯。- 明確定義訊息契約(型別/協定),並透過加上關聯 ID讓請求/回應設計更可靠。
- 適用於需於所有分頁統一處理的流程,例如WebSocket 連線多工分享或序列化 IndexedDB 存取。
- 對打包器、型別定義、重連機制等細節進行調整,能大幅提升維護性與用戶體驗。
您可以在我們的 YouTube 頻道上使用 Visual Studio Code 來跟隨上述文章一起學習。 請也查看我們的 YouTube 頻道。