Shared Worker dalam JavaScript

Shared Worker dalam JavaScript

Artikel ini menerangkan tentang Shared Worker dalam JavaScript.

Kami akan menerangkan semuanya dari asas Shared Worker sehingga kes penggunaan praktikal secara langkah demi langkah.

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 dalam JavaScript

Apakah itu Shared Worker?

Shared Worker ialah thread pekerja yang boleh dikongsi antara beberapa halaman (tab, tetingkap, iframe, dsb.) dalam asal yang sama. Berbeza dengan Dedicated Worker yang khusus untuk satu halaman, ciri utama ialah satu proses latar belakang boleh dikongsi antara beberapa halaman. Beberapa kes penggunaan biasa adalah seperti berikut:.

  • Anda boleh berkongsi satu sambungan WebSocket di antara beberapa tab, mengurangkan jumlah sambungan dan memusatkan proses sambungan semula.
  • Anda boleh menyelaraskan keadaan antara tab (pengurusan terpusat untuk Pub/Sub atau storan).
  • Anda boleh menyusun proses operasi IndexedDB, sebagai perantara akses serentak.
  • Anda boleh mengelakkan pelaksanaan berganda proses yang memerlukan pengiraan berat.

Shared Worker mempunyai peranan yang berbeza daripada Service Worker. Service Worker berperanan utama sebagai proksi rangkaian, manakala Shared Worker memberi tumpuan kepada pelaksanaan pengiraan arbitrari atau pengurusan keadaan yang dikongsi antara halaman.

API Asas dan Kitaran Hayat

Mewujudkan (Thread Utama)

 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 });
  • Kod ini menunjukkan cara membuat Shared Worker dan menghantar/menerima mesej melalui portnya.

Penerima (dalam skop 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)
  • Kod ini menunjukkan bagaimana, di dalam Shared Worker, satu MessagePort diterima untuk setiap sambungan dan memproses serta membalas mesej daripada klien.

Kitaran Hayat bagi Shared Worker

Shared Worker dimulakan apabila sambungan pertama diwujudkan dan mungkin ditamatkan apabila port terakhir ditutup. Sambungan akan ditutup apabila halaman dimuat semula/ditutup, dan pekerja akan diwujudkan semula jika diperlukan.

Shared Worker ialah "skrip jangka panjang yang dikongsi dalam pelayar". Jika anda tidak prihatin terhadap kitar hayat Shared Worker, masalah seperti kebocoran sumber, penyimpanan keadaan usang, dan permulaan semula yang tidak disengajakan lebih cenderung untuk berlaku.

Cuba Sendiri: Siaran di Antara Tab

Ini adalah pelaksanaan minimum dimana beberapa tab disambungkan ke Shared Worker yang sama dan setiap mesej yang dihantar akan disiarkan ke semua 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});
  • Kod ini menyambung ke Shared Worker, melaksanakan penerimaan/paparan mesej, menghantar dari borang dan menghantar ping dengan menekan butang.

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};
  • Kod ini melaksanakan ciri pub/sub ringkas dalam Shared Worker untuk menyampaikan mesej antara beberapa klien.

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>
  • Jika anda membuka halaman ini pada beberapa tab, mesej yang dihantar dari mana-mana tab akan dimaklumkan kepada yang lain.

Reka Bentuk Mesej: Permintaan/Tindak Balas dan Correlation ID

Apabila beberapa klien berinteraksi dengan Shared Worker, anda sering ingin mengenal pasti tindak balas yang sepadan dengan permintaan yang mana. Oleh itu, adalah amalan standard untuk memasukkan ID korelasi supaya respons dapat dikaitkan dengan permintaan.

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'
  • Kod ini melaksanakan klien RPC ringkas yang boleh membuat panggilan kaedah tak segerak kepada Shared Worker. Di sini, pelayan RPC (Remote Procedure Call server) merujuk kepada pelayan yang menyediakan mekanisme untuk memanggil fungsi dan prosedur dari program atau proses lain.
  • Dalam contoh ini, id hanya dinaikkan secara berturutan, tetapi anda juga boleh menggunakan UUID, rentetan rawak, atau menggabungkan cop masa dengan kaunter untuk mencipta kunci unik.

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};
  • Kod ini melaksanakan pelayan RPC ringkas yang menjalankan pemprosesan mengikut method dan menghantar semula hasil bersama dengan ID permintaan.

Pola Penggunaan Biasa

Terdapat beberapa pola praktikal apabila menggunakan Shared Worker, seperti berikut:.

Multiplexing WebSocket Tunggal (Dikongsi antara Tab)

Jika setiap tab membuka sambungan WebSocket sendiri, ia akan menjejaskan beban pelayan dan had sambungan. Letakkan hanya satu WebSocket dalam Shared Worker, dan setiap tab menghantar/menerima mesej melalui pekerja tersebut.

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}
  • Kod ini melaksanakan proses menghantar/menerima mesej ke pelayan WebSocket dan menerima notifikasi status sambungan melalui 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};
  • Kod ini melaksanakan satu mekanisme dalam Shared Worker untuk berkongsi satu sambungan WebSocket, menyiarkan status sambungan dan data yang diterima ke semua klien, sambil menghantar permintaan keluar setiap klien ke pelayan melalui WebSocket.

  • Dengan memusatkan strategi sambungan semula dan pengekosan mesej yang belum dihantar dalam Shared Worker, tingkah laku menjadi konsisten di semua tab.

Pemediasi IndexedDB (Penyusunan)

Apabila mengakses DB yang sama dari beberapa tab, jika anda ingin mengelakkan konflik dari transaksi serentak atau menunggu kunci, anda boleh menjajarkan mereka dalam Shared Worker dan memprosesnya secara bersiri.

 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};
  • Kod ini melaksanakan mekanisme di mana operasi baca/tulis ke IndexedDB dijajarkan melalui Shared Worker, menyusun akses daripada beberapa tab untuk mengelakkan konflik dan perebutan kunci.

Petua Pemodularan dan Bundler

Jika anda ingin menulis skrip Shared Worker anda sebagai Modul ES, tingkah laku dan sokongan boleh berbeza mengikut persekitaran. Dalam amalan, adalah lebih selamat untuk memilih salah satu daripada berikut:.

  • Tulis dalam format pekerja klasik, dan gunakan importScripts() untuk memuatkan kebergantungan jika perlu.
  • Gunakan ciri entri pekerja pada bundler anda (seperti Vite / Webpack / esbuild, dsb.), dan bina bundle berasingan untuk Shared Worker semasa proses build.

Petua untuk Pengendalian Ralat, Pengesanan Pemutusan, dan Keteguhan

Pertimbangkan perkara berikut untuk pengendalian ralat dan keteguhan:.

  • Mengendalikan Penghantaran Sebelum Sambungan Dijalinkan Jajarkan mesej yang tiba sebelum port bersedia.

  • Pengesanan Pemutusan MessagePort tidak mempunyai pengendali onclose standard. Di sisi utama, hantar mesej {type: 'bye'} dengan port.postMessage semasa beforeunload, atau nyatakan dengan jelas dalam protokol untuk memastikan pekerja dibersihkan.

  • Sambungan Semula Apabila halaman dimuat semula atau tab dibuka semula, satu port baru diwujudkan. Sediakan mesej penyelarasan awal (untuk menghantar keadaan lengkap sekaligus).

  • Tekanan Balik (Backpressure) Semasa penyiaran berat berlaku, beralih ke throttling/debouncing atau menghantar snapshot.

  • Keselamatan Shared Worker pada asasnya hanya dikongsi dalam asal yang sama. Jika anda meletakkan maklumat rahsia dalam pekerja, pertimbangkan mereka bentuk pengesahan di pihak pemanggil menggunakan token atau kaedah seumpamanya.

Cara Menggunakan Dedicated Worker, Shared Worker, dan Service Worker dengan Betul

Setiap jenis Worker mempunyai ciri-ciri berikut:.

  • Dedicated Worker Dedicated Worker hanya digunakan oleh satu halaman sahaja. Ia membolehkan pemisahan 1:1 antara pengiraan dan UI.

  • Shared Worker Shared Worker boleh dikongsi merentas beberapa halaman yang mempunyai asal (origin) yang sama. Ia sangat sesuai untuk komunikasi antara tab dan perkongsian satu sambungan sahaja.

  • Service Worker Service Worker boleh digunakan untuk proksi rangkaian, caching, operasi luar talian, notifikasi tolak (push notifications), dan penyelarasan latar belakang. Kelebihannya adalah kebolehan untuk memintas permintaan fetch.

Sebagai panduan umum: gunakan Shared Worker untuk 'perkongsian maklumat dan pemprosesan arbitrari antara tab', Service Worker untuk 'kawalan rangkaian', dan Dedicated Worker jika anda hanya mahu memindahkan pemprosesan berat dari UI.

Perangkap Lazim

Apabila menggunakan Shared Worker, anda perlu berhati-hati dengan perkara-perkara berikut.

  • Terlupa untuk memanggil start() atau seruan yang tidak perlu Apabila menggunakan port.addEventListener('message', ...), anda wajib memanggil port.start(). Ini tidak perlu jika anda menggunakan port.onmessage = ....

  • Penyebaran Tanpa Had Beban akan meningkat apabila bilangan tab bertambah. Anda boleh mengurangkan beban dengan melaksanakan differential delivery atau topik langganan (penapisan mengikut topik).

  • Kos Salinan Objek postMessage akan menduplikasi data. Untuk data yang besar, pertimbangkan untuk menghantarnya sebagai Transferable (seperti ArrayBuffer) atau menggunakan memori kongsi (SharedArrayBuffer) bersama dengan Atomics.

  • Jangka Hayat dan Inisialisasi Semula Shared Worker mungkin akan ditamatkan apabila klien terakhir putus sambungan. Reka bentuk inisialisasi dengan betul untuk sambungan pertama dan permulaan semula bagi mengelakkan kesan sampingan dan pepijat.

Ringkasan

  • Shared Worker adalah tempat untuk melaksanakan logik tersuai jangka panjang yang dikongsi merentas banyak halaman.
  • Jelaskan kontrak mesej (jenis/protokol) dan jadikan reka bentuk permintaan/tindak balas anda kukuh dengan melampirkan correlation IDs.
  • Ia sangat sesuai untuk proses yang berfungsi dengan lebih baik apabila disatukan di semua tab, seperti multiplexing sambungan WebSocket atau mensiri akses ke IndexedDB.
  • Penalaan terperinci seperti bundlers, definisi jenis, dan penyambungan semula dapat meningkatkan penyelenggaraan dan pengalaman pengguna dengan ketara.

Anda boleh mengikuti artikel di atas menggunakan Visual Studio Code di saluran YouTube kami. Sila lihat juga saluran YouTube kami.

YouTube Video