Shared Worker trong JavaScript
Bài viết này giải thích về Shared Worker
trong JavaScript.
Chúng tôi sẽ giải thích từng bước mọi thứ từ cơ bản về Shared Worker
đến các trường hợp sử dụng thực tế.
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>
Shared Worker trong JavaScript
Shared Worker
là gì?
Shared Worker
là một luồng làm việc có thể được chia sẻ giữa nhiều trang (tab, cửa sổ, iframe, v.v.) trong cùng một nguồn gốc (origin). Khác với Dedicated Worker
dành riêng cho từng trang, điểm nổi bật chính là một tiến trình nền duy nhất có thể được chia sẻ giữa nhiều trang. Các trường hợp sử dụng điển hình bao gồm:.
- Bạn có thể chia sẻ một kết nối WebSocket duy nhất giữa nhiều tab, giảm số lượng kết nối và tập trung việc kết nối lại.
- Bạn có thể đồng bộ trạng thái giữa các tab (quản lý tập trung cho Pub/Sub hoặc stores).
- Bạn có thể tuần tự hóa các thao tác với IndexedDB, giúp điều phối truy cập đồng thời.
- Bạn có thể ngăn chặn việc thực hiện lặp lại các quá trình tốn nhiều tài nguyên tính toán.
Shared Worker
có vai trò khác vớiService Worker
.Service Worker
chủ yếu hoạt động như một proxy mạng, trong khiShared Worker
tập trung vào thực hiện các phép tính tùy ý hoặc quản lý trạng thái được chia sẻ giữa các trang.
API cơ bản và Chu kỳ sống
Tạo (luồng chính)
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 });
- Đoạn mã này minh họa cách tạo một
Shared Worker
và gửi/nhận tin nhắn qua port của nó.
Trình nhận (trong phạm vi của 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)
- Đoạn mã này trình bày cách, bên trong
Shared Worker
, mộtMessagePort
được nhận cho mỗi kết nối và xử lý cũng như trả lời các tin nhắn từ client.
Chu kỳ sống của Shared Worker
Shared Worker
bắt đầu khi kết nối đầu tiên được thiết lập và có thể kết thúc khi cổng cuối cùng được đóng. Kết nối sẽ đóng khi tải lại/đóng trang, và worker sẽ được tạo lại nếu cần.
Shared Worker
là một "script có vòng đời dài được chia sẻ trong trình duyệt". Nếu bạn không chú ý đến vòng đời của Shared Worker
, các vấn đề như rò rỉ tài nguyên, trạng thái cũ còn lưu lại, và khởi động lại ngoài ý muốn có thể xảy ra thường xuyên hơn.
Thử nghiệm: Gửi phát (broadcast) giữa các tab
Đây là một ví dụ triển khai tối giản, nơi nhiều tab kết nối đến cùng một Shared Worker
và bất cứ tin nhắn nào gửi đi đều được gửi phát đến tất cả các tab.
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});
- Đoạn mã này kết nối với một
Shared Worker
, thực hiện nhận/hiển thị tin nhắn, gửi tin từ form nhập và gửi ping bằng cách nhấn nút.
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};
- Đoạn mã này triển khai một tính năng pub/sub đơn giản bên trong
Shared Worker
để chuyển tiếp tin nhắn giữa nhiều client.
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>
- Nếu bạn mở trang này ở nhiều tab, bất kỳ tin nhắn nào gửi đi từ một tab cũng sẽ được thông báo tới các tab khác.
Thiết kế tin nhắn: Request/Response và Correlation ID
Khi nhiều client tương tác với một Shared Worker
, bạn thường muốn xác định đáp ứng nào tương ứng với yêu cầu nào. Do đó, việc sử dụng một mã tương quan (correlation ID) để liên kết phản hồi với yêu cầu là thông lệ tiêu chuẩn.
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'
- Đoạn mã này triển khai một client RPC đơn giản có thể gọi phương thức bất đồng bộ tới
Shared Worker
. Ở đây, máy chủ RPC (máy chủ gọi thủ tục từ xa) là một máy chủ cung cấp cơ chế để gọi các hàm và thủ tục từ các chương trình hoặc tiến trình khác. - Trong ví dụ này,
id
chỉ cần tăng dần, nhưng bạn cũng có thể sử dụng UUID, chuỗi ngẫu nhiên, hoặc kết hợp timestamp với bộ đếm để tạo khóa duy nhất.
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};
- Đoạn mã này triển khai một máy chủ RPC đơn giản, thực thi xử lý theo
method
và gửi lại kết quả cùng với ID của yêu cầu.
Các mô hình sử dụng điển hình
Có một số mô hình sử dụng thực tế khi dùng Shared Worker
, ví dụ như sau:.
Đa kênh hóa một WebSocket duy nhất (chia sẻ giữa các tab)
Nếu mỗi tab mở một kết nối WebSocket riêng, nó sẽ ảnh hưởng đến tải máy chủ và giới hạn kết nối. Đặt chỉ một WebSocket trong Shared Worker
, và mỗi tab gửi/nhận tin nhắn thông qua 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}
- Đoạn mã này triển khai quá trình gửi/nhận tin nhắn tới máy chủ WebSocket và nhận thông báo trạng thái kết nối qua
Shared Worker
.
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};
-
Đoạn mã này triển khai một cơ chế bên trong
Shared Worker
để chia sẻ một kết nối WebSocket duy nhất, gửi phát trạng thái kết nối và dữ liệu nhận được tới tất cả client, đồng thời gửi các yêu cầu đi của từng client tới máy chủ qua WebSocket. -
Bằng cách tập trung các chiến lược kết nối lại và xếp hàng tin nhắn chưa gửi trong
Shared Worker
, hành vi sẽ nhất quán trên tất cả các tab.
Môi giới IndexedDB (tuần tự hóa)
Khi truy cập cùng một cơ sở dữ liệu từ nhiều tab, nếu muốn tránh xung đột từ các giao dịch đồng thời hoặc chờ khóa, bạn có thể xếp hàng các thao tác này trong Shared Worker
và xử lý tuần tự.
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};
- Đoạn mã này triển khai một cơ chế mà các thao tác đọc/ghi vào IndexedDB được xếp hàng qua
Shared Worker
, tuần tự hóa truy cập từ nhiều tab để tránh xung đột và tranh chấp khóa.
Lời khuyên về mô-đun hóa và công cụ đóng gói (bundler)
Nếu bạn muốn viết script Shared Worker
dưới dạng ES Module, hành vi và khả năng hỗ trợ có thể khác nhau tùy môi trường. Trong thực tế, an toàn hơn khi chọn một trong những cách sau:.
- Viết theo định dạng worker cổ điển và sử dụng
importScripts()
để tải các phụ thuộc nếu cần. - Sử dụng tính năng worker entry của công cụ đóng gói như Vite / Webpack / esbuild, v.v. và tạo một bundle riêng cho
Shared Worker
khi build.
Mẹo xử lý lỗi, phát hiện ngắt kết nối và tăng tính ổn định
Hãy xem xét các điểm sau để xử lý lỗi và tăng tính ổn định:.
-
Xử lý việc gửi trước khi kết nối được thiết lập Xếp hàng các tin nhắn đến trước khi port sẵn sàng.
-
Phát hiện ngắt kết nối
MessagePort
không có trình xử lýonclose
tiêu chuẩn. Ở phía chính, gửi một tin nhắn{type: 'bye'}
bằngport.postMessage
trong sự kiệnbeforeunload
, hoặc quy định rõ ràng trong giao thức để đảm bảo worker được dọn dẹp. -
Kết nối lại Khi một trang được tải lại hoặc tab được mở lại, một port mới sẽ được tạo. Chuẩn bị một tin nhắn đồng bộ hóa ban đầu (để gửi toàn bộ trạng thái cùng lúc).
-
Áp lực truyền tải (backpressure) Khi truyền phát liên tục với lưu lượng lớn, hãy chuyển sang giới hạn/tối ưu hóa gửi hoặc gửi các bản snapshot.
-
Bảo mật
Shared Worker
về cơ bản được chia sẻ trong cùng một origin. Nếu đặt thông tin mật bên trong worker, hãy xem xét thiết kế bước xác minh với token hoặc phương thức tương tự cho phía gọi.
Cách sử dụng Dedicated Worker
, Shared Worker
và Service Worker
một cách hợp lý
Mỗi loại Worker đều có các đặc điểm sau:.
-
Dedicated Worker
Dedicated Worker
chỉ được sử dụng cho một trang duy nhất. Nó cho phép tách biệt xử lý tính toán ra khỏi giao diện người dùng theo kiểu một-một. -
Shared Worker
Shared Worker
có thể được sử dụng chung cho nhiều trang có cùng origin. Nó lý tưởng cho giao tiếp giữa các tab và chia sẻ một kết nối duy nhất. -
Service Worker
Service Worker
có thể được sử dụng để làm proxy mạng, lưu vào bộ nhớ đệm, hoạt động ngoại tuyến, thông báo đẩy và đồng bộ hóa nền. Điểm mạnh của nó là khả năng chặn các yêu cầu fetch.
Mẹo sử dụng: hãy dùng
Shared Worker
cho 'chia sẻ thông tin và xử lý tùy ý giữa các tab',Service Worker
cho 'kiểm soát mạng', vàDedicated Worker
nếu bạn chỉ muốn tách việc xử lý nặng khỏi giao diện người dùng.
Các sai lầm thường gặp
Khi sử dụng Shared Worker
, bạn cần chú ý các điểm sau.
-
Quên gọi
start()
hoặc gọi không cần thiết Khi dùngport.addEventListener('message', ...)
, bạn phải gọiport.start()
. Bạn không cần làm điều này nếu dùngport.onmessage = ...
. -
Phát sóng không kiểm soát Tải hệ thống tăng lên khi số lượng tab tăng. Bạn có thể giảm tải bằng cách triển khai giao nhận phân biệt hoặc chủ đề đăng ký (lọc theo chủ đề).
-
Chi phí sao chép đối tượng
postMessage
sẽ sao chép dữ liệu. Với dữ liệu lớn, hãy cân nhắc gửi dưới dạng Transferable (nhưArrayBuffer
) hoặc sử dụng bộ nhớ chia sẻ (SharedArrayBuffer
) cùng vớiAtomics
. -
Vòng đời và khởi tạo lại
Shared Worker
có thể kết thúc khi client cuối cùng ngắt kết nối. Thiết kế khởi tạo hợp lý cho kết nối đầu tiên và khi khởi động lại để tránh tác dụng phụ và lỗi.
Tóm tắt
Shared Worker
là nơi thực hiện logic tùy chỉnh tồn tại lâu dài giữa nhiều trang.- Làm rõ hợp đồng thông điệp (kiểu/giao thức) và làm cho thiết kế request/response mạnh mẽ hơn bằng cách gắn kèm ID tương quan.
- Nó lý tưởng cho các quy trình hoạt động hiệu quả hơn khi được thống nhất trên tất cả các tab, như phối ghép kết nối WebSocket hoặc tuần tự hóa truy cập IndexedDB.
- Tinh chỉnh các chi tiết như trình đóng gói (bundler), định nghĩa kiểu dữ liệu và kết nối lại có thể cải thiện khả năng bảo trì và trải nghiệm người dùng một cách đáng kể.
Bạn có thể làm theo bài viết trên bằng cách sử dụng Visual Studio Code trên kênh YouTube của chúng tôi. Vui lòng ghé thăm kênh YouTube.