前端練習 - 真·多執行緒背景作業 WebWorker
| | | 0 | |
WebWorker 的誕生可以追溯到 2009 年參考,說起來已是項古老的技術,Firefox 3.5 最早開始實作,如今所有主流瀏覽器都已支援,除非你還需要支援 IE (拍拍),大可安心使用。
不過,需要動用 WebWorker 多屬進階應用情境,大多也有替代方法可實現,所以寫了幾十年網頁從沒在實際專案用過 WebWorker 也是情有可原,與不上進、沒見識無關 (我都這麼想讓自己好過一點 😛)。翻了一下,四年前我有寫過一篇 發揮 JavaScript 多執行緒威力 - Web Worker,當時跳過了基本教學,下場是當被問到 WebWorker 怎麼寫時,我的腦袋沒半點概念,決定乖乖補上一篇筆記。
簡單來說,我們可以把 WebWorker 想成「真正在另一條執行緒執行的背景作業」,執行時完全不會卡住網頁的執行。(註:瀏覽器基於 Event Loop,setTimeout() 也會佔用主執行緒而非真的在背景執行,還是會卡住 UI,有興趣可參考 舊文的展示) WebWorker 可以使用 XMLHttpRequest(XHR)/fetch 存取伺服器端資源(與從網頁執行一樣受同源政策限制),但禁止直接存取 DOM 或 window 物件,MDN 文件有份 WebWorker 可以使用的 Browser Web API 清單。網頁上的程式不能直接呼叫 WebWorker 裡的函式,只能透過 postMessage(data) 方法溝通,WebWorker 實作 onmessage(e) 事件由 e.data 取得傳入參數,處理結果再用 postMessage() 傳回呼叫端。
WebWorker 的程式要寫成獨立 .js,網頁端以 new Worker('file-path.js') 建立執行個體(假設指派給變數 worker),先掛載 worker.onmessage 事件接收 WebWorker 回傳結果,接著便可用 worker.postMessage() 傳送參數觸發 WebWorker 中的邏輯。
以下是個簡單範例: 線上展示
sample1-webwork.js
self.onmessage = function(e) {
const data = e.data;
if (data === 'error') {
throw new Error('Throw error for testing');
}
const result = `Echo: ${data}`;
self.postMessage(result);
};
sample1.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
}
h1 {
color: #333;
}
button {
padding: 6px;
cursor: pointer;
}
#output {
margin-top: 20px;
padding: 10px;
border: 1px solid #ccc;
background-color: #f9f9f9;
}
.error {
color: red;
}
</style>
</head>
<body>
<h1>WebWorker 範例一</h1>
<button onclick="worker.postMessage('Hello, Worker!')">發送訊息</button>
<button onclick="worker.postMessage('error')">觸發錯誤</button>
<div id="output"></div>
<script>
const output = document.getElementById('output');
function showMessage(message, css) {
output.innerHTML += `<p class="${css}">${message}</p>`;
}
const worker = new Worker('sample1-webworker.js');
worker.onerror = function(e) {
showMessage(e.message, 'error');
};
worker.onmessage = function(e) {
const result = e.data;
showMessage(result);
};
</script>
</body>
</html>

如果不想多一個 .js 檔,也可寫成網頁內的 <script> 區塊,用程式讀取內容轉成 Blob 再轉 DataURI 當作 URL 傳給 Worker 建構式,效果與載入 .js 相同。線上展示
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
/* 略 */
</style>
<script id="worker-script" type="javascript/worker">
self.onmessage = function(e) {
const data = e.data;
if (data === 'error') {
throw new Error('Throw error for testing');
}
const result = `Echo: ${data}`;
self.postMessage(result);
};
</script>
</head>
<body>
<h1>WebWorker 範例二</h1>
<button onclick="worker.postMessage('Hello, Worker!')">發送訊息</button>
<button onclick="worker.postMessage('error')">觸發錯誤</button>
<div id="output"></div>
<script>
const output = document.getElementById('output');
function showMessage(message, css) {
output.innerHTML += `<p class="${css}">${message}</p>`;
}
// 讀取 WebWorker 程式碼轉成 URL 當成 Worker 建構參數
const workerScript = document.getElementById('worker-script').textContent;
const blob = new Blob([workerScript], { type: 'application/javascript' });
const worker = new Worker(URL.createObjectURL(blob));
worker.onerror = function(e) {
showMessage(e.message, 'error');
};
worker.onmessage = function(e) {
const result = e.data;
showMessage(result);
};
</script>
</body>
</html>
Web Worker 也支援多網頁共用,如此可實現狀態資料或資源的共享,做法是用 new SharedWorker(<js-path>) 建立 Shared Worker,而 Shared Worker 要實作 onconnect() 事件,用 port 來管理對不同來源的連線。我寫了一個簡單的聊天室當範例:
ex-shared-web-worker.js
// 共享變數記錄狀態
let connectedPorts = [];
let sharedData = {
messages: [],
};
onconnect = function(e) {
const port = e.ports[0];
connectedPorts.push(port);
port.postMessage({
command: 'init',
messages: sharedData.messages
});
port.onmessage = function(e) {
const data = e.data;
switch (data.command) {
case 'speak':
const newMessage = {
text: data.message.text,
user: data.message.user,
timestamp: new Date().toLocaleTimeString('sv')
};
sharedData.messages.push(newMessage);
broadcastToAllPorts({
command: 'message',
message: newMessage
});
break;
case 'status':
port.postMessage({
command: 'statusInfo',
info: ` ${connectedPorts.length} ports connected`
});
break;
case 'close':
port.close();
removePort(port);
break;
}
};
port.onerror = function(error) {
console.error('Port error:', error);
removePort(port);
};
//port.start();
};
// 廣播訊息給所有連接的頁面
function broadcastToAllPorts(message) {
connectedPorts.forEach(port => {
try {
port.postMessage(message);
} catch (error) {
console.error('Error sending message to port:', error);
removePort(port);
}
});
}
// 移除已關閉的 port
function removePort(portToRemove) {
const index = connectedPorts.indexOf(portToRemove);
if (index > -1) {
connectedPorts.splice(index, 1);
}
}
呼叫端可輸入姓名跟文字,按 Send 送出訊息給所有客戶端。
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<title>Shared Web Worker Example</title>
<style>
html, body {
font-family: Verdana, Geneva, Tahoma, sans-serif;
margin: 0;
padding: 6px;
font-size: 9pt;
}
#output {
margin-top: 10px;
padding: 6px;
min-height: 50px;;
background-color: #eee;
.message {
color: #333;
}
}
</style>
</head>
<body>
<form id="msgForm">
<table>
<tr>
<td>
<label for="user">User:</label>
</td>
<td>
<input type="text" id="user" required>
</td>
</tr>
<tr>
<td>
<label for="message">Message:</label>
</td>
<td>
<input type="text" id="message" required>
</td>
</tr>
</table>
<button type="submit">Send</button>
</form>
<div id="output"></div>
<script>
const user = 'User' + Math.floor(Math.random() * 1000);
const userElement = document.getElementById('user');
userElement.value = user;
const msgElement = document.getElementById('message');
msgElement.value = 'Hello';
const output = document.getElementById('output');
function showMessage(message, css) {
const msg = document.createElement('div');
msg.className = css;
msg.textContent = `${message.timestamp} [${message.user}]: ${message.text}`;
output.appendChild(msg);
}
// 載入 Shared Web Worker
const worker = new SharedWorker('ex3-shared-webworker.js');
worker.port.start();
// 顯示收到的訊息
worker.port.onmessage = function (e) {
const data = e.data;
switch (data.command) {
case 'init':
data.messages.forEach(msg => {
showMessage(msg, 'message');
});
break;
case 'message':
const msg = data.message;
showMessage(msg, 'message');
break;
}
};
// 發送訊息
document.getElementById('msgForm').addEventListener('submit', function (e) {
e.preventDefault();
const user = userElement.value;
const text = msgElement.value;
worker.port.postMessage({ command: 'speak', message: { user, text } });
});
// 簡便狀態更新:退出或關閉網頁前送出指令通知 Shared Worker 關閉連線
// (嚴謹做法:建立 Heartbeat 機制定期送訊息給 Shared Worker 主動檢查客戶端是否活著)
window.addEventListener('beforeunload', function () {
worker.port.postMessage({ command: 'close' });
worker.port.close();
});
</script>
</body>
</html>
最後寫一個有兩個 IFrame 可載入或退出網頁:線上展示

最後,補充一些小技巧。
- WebWorker 與網頁間傳遞參數時會被序列化再還原,無法直接共享記憶體中的物件個體。例如:用
const msg = { text: 'A' }; worker.postMessage(msg);將 msg 傳給 WebWorker 後,WebWorker 拿到的是 msg 的複製品,對 msg.text 的修改不會影響網頁端的 msg 物件。
若你嘗試傳遞非資料性質的物件,則會出現錯誤。

- 當需要傳送數十 MB 到數百 MB 的大型資料,每次都序列化再反序列化的成本驚人,新版瀏覽器支援直接傳送 ArrayBuffer、File、Blob 等 可移轉物件 / Transferable Object (有實作 Transferable 介面,例如 Uint8Array.buffer) 給 Web Worker,傳送效能可提高近 50 倍(300ms vs 6.6ms)。不過為避免雙方同時存取物件衍生執行緒安全問題,主網頁一旦將可移轉物件傳送到 Web Worker 後,便不能再使用它。
- Web Worker 跟一般 JavaScript 一樣可以用 F12 Dev Tools 偵錯,你可以在來源 (Source) 的頁面樹狀圖找到它:

Shared Worker 的話,請另開新頁籤輸入edge://inspect#wokrers或chrome://inspect#workers找到它:

This blog post explores the usage of Web Workers in modern browsers, highlighting their ability to run scripts in background threads, improving performance without blocking the UI. It provides examples of basic and shared Web Workers, demonstrating message passing and shared state management across multiple pages.
Comments
Be the first to post a comment