JavaScript中的共享工作者

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 &amp; 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 WorkerService 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 WorkerShared WorkerService Worker

每種 Worker 具有以下特點:。

  • Dedicated Worker Dedicated Worker 僅限單一頁面使用。它能將計算任務與 UI 進行一對一的分離。

  • Shared Worker Shared Worker 可在同一來源的多個頁面間共享。非常適合用於分頁間通訊共用單一連線

  • Service Worker Service 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 頻道。

YouTube Video