Shared Worker trong JavaScript

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 &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>

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ới Service Worker. Service Worker chủ yếu hoạt động như một proxy mạng, trong khi Shared 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ột MessagePort đượ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 Workerbấ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ằng port.postMessage trong sự kiện beforeunload, 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 WorkerService 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 tabchia 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ùng port.addEventListener('message', ...), bạn phải gọi port.start(). Bạn không cần làm điều này nếu dùng port.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ới Atomics.

  • 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.

YouTube Video