`Shared Worker` dalam TypeScript
Artikel ini menerangkan Shared Worker
dalam TypeScript.
Kami akan menerangkan secara terperinci cara Shared Worker
berfungsi dan cara menggunakannya dalam amalan, beserta contoh kod TypeScript.
YouTube Video
Shared Worker
dalam TypeScript
Shared Worker
ialah proses worker tunggal yang dikongsi merentasi berbilang tab, tetingkap dan iframe pada origin yang sama. Dengan menggunakannya, anda boleh mengendalikan keadaan (state) dan sumber yang dikongsi merentasi berbilang tab pelayar.
Sebagai contoh, anda boleh melaksanakan dengan cekap sambungan WebSocket yang dikongsi, cache dan pemprosesan giliran yang disegerakkan merentasi tab, serta pengecualian bersama (mutual exclusion).
Tidak seperti Dedicated Worker
, Shared Worker
menerima berbilang MessagePort
melalui peristiwa onconnect
dan boleh memultipleks komunikasi dengan berbilang klien.
Kes apabila anda harus memilih Shared Worker
Dalam kes berikut, menggunakan Shared Worker
adalah sesuai.
- Apabila anda memerlukan keadaan (state) dikongsi atau pengecualian bersama merentasi tab
- Apabila anda ingin berkongsi satu sambungan WebSocket atau akses IndexedDB
- Apabila anda perlu memaklumkan semua tab (broadcast)
- Apabila anda mahu memusatkan pemprosesan berat untuk menjimatkan sumber
Sebaliknya, dalam kes berikut, pendekatan lain adalah lebih sesuai.
- Apabila anda memerlukan kawalan cache atau sokongan luar talian, anda boleh menggunakan
Service Worker
. - Untuk pemprosesan berat yang terhad pada satu tab, anda boleh menggunakan
Dedicated Worker
.
Langkah pelaksanaan untuk Shared Worker
Di sini, kami akan melaksanakan perkara berikut langkah demi langkah menggunakan TypeScript.
- Protokol mesej type-safe
- Permintaan/tindak balas berasaskan Promise (RPC)
- Siaran kepada semua tab
- Nadi (heartbeat) dan pembersihan klien
Persediaan persekitaran
Cipta konfigurasi untuk mengkompil setiap fail sumber yang menggunakan Shared Worker
.
1{
2 "compilerOptions": {
3 "target": "ES2020",
4 "module": "ES2020",
5 "lib": ["ES2020", "dom", "WebWorker"],
6 "moduleResolution": "Bundler",
7 "strict": true,
8 "noEmitOnError": true,
9 "outDir": "out",
10 "skipLibCheck": true
11 },
12 "include": ["src"]
13}
- Dalam
tsconfig-src.json
, dayakan definisi jenis DOM dan Web Worker supaya kod boleh dikompil dengan selamat.
Mentakrifkan protokol mesej
Asas komunikasi ialah kontrak mesej bertaip. Mentakrifkannya terlebih dahulu menjadikan komunikasi seterusnya selamat dan mudah untuk dikembangkan.
1// worker-protocol.ts
2// Define discriminated unions for structured messages
3
4export type RequestAction =
5 | { type: 'ping' }
6 | { type: 'echo'; payload: string }
7 | { type: 'getTime' }
8 | { type: 'set'; key: string; value: unknown }
9 | { type: 'get'; key: string }
10 | { type: 'broadcast'; channel: string; payload: unknown }
11 | { type: 'lock.acquire'; key: string; timeoutMs?: number }
12 | { type: 'lock.release'; key: string };
RequestAction
ialah kesatuan terdiskriminasi (tagged union) yang mewakili jenis permintaan yang dihantar kepada worker dan mentakrifkan operasi sepertiping
,get
danbroadcast
.
1export interface RequestMessage {
2 kind: 'request';
3 id: string;
4 from: string;
5 action: RequestAction;
6}
RequestMessage
mentakrif struktur mesej permintaan yang dihantar dari klien kepada worker.
1export interface ResponseMessage {
2 kind: 'response';
3 id: string;
4 ok: boolean;
5 result?: unknown;
6 error?: string;
7}
ResponseMessage
mentakrif struktur mesej respons yang dipulangkan daripada worker kepada klien.
1export interface BroadcastMessage {
2 kind: 'broadcast';
3 channel: string;
4 payload: unknown;
5 from: string;
6}
BroadcastMessage
mentakrif struktur mesej siaran yang dihantar oleh worker kepada klien lain.
1export type WorkerInMessage =
2 | RequestMessage
3 | { kind: 'heartbeat'; from: string }
4 | { kind: 'bye'; from: string };
WorkerInMessage
ialah jenis yang mewakili semua mesej yang diterima oleh worker, seperti permintaan, heartbeat, dan pemberitahuan pemutusan sambungan.
1export type WorkerOutMessage = ResponseMessage | BroadcastMessage;
WorkerOutMessage
ialah jenis yang mewakili mesej respons atau siaran yang dihantar oleh worker kepada klien.
1export const randomId = () => Math.random().toString(36).slice(2);
randomId
ialah fungsi yang menjana rentetan alfanumerik rawak untuk digunakan bagi ID mesej dan seumpamanya.
Melaksanakan Shared Worker
Dalam shared-worker.ts
, daftar tab yang bersambung melalui peristiwa onconnect
dan kendalikan mesej.
1// shared-worker.ts
2/// <reference lib="webworker" />
- Arahan ini mengarahkan TypeScript memuatkan definisi jenis untuk Web Worker.
1import {
2 WorkerInMessage,
3 WorkerOutMessage,
4 RequestMessage,
5 ResponseMessage,
6} from './worker-protocol.js';
- Mengimport definisi jenis yang digunakan untuk komunikasi worker.
1export default {};
2declare const self: SharedWorkerGlobalScope;
- Mengisytiharkan dengan jelas bahawa
self
ialah skop global bagiShared Worker
.
1type Client = {
2 id: string;
3 port: MessagePort;
4 lastBeat: number;
5};
Client
ialah jenis yang mewakili pengecam klien, port komunikasi, dan cap masa heartbeat terakhir setiap klien.
1const clients = new Map<string, Client>();
2const kv = new Map<string, unknown>();
3const locks = new Map<string, string>();
4const HEARTBEAT_TIMEOUT = 30_000;
- Mengurus senarai klien yang bersambung, storan pasangan kunci-nilai, keadaan kunci (lock), dan tempoh tamat masa.
1function send(port: MessagePort, msg: WorkerOutMessage) {
2 port.postMessage(msg);
3}
send
ialah fungsi utiliti yang menghantar mesej ke port yang ditetapkan.
1function respond(req: RequestMessage, ok: boolean, result?: unknown, error?: string): ResponseMessage {
2 return { kind: 'response', id: req.id, ok, result, error };
3}
respond
menjana mesej respons untuk sesuatu permintaan.
1function broadcast(from: string, channel: string, payload: unknown) {
2 for (const [id, c] of clients) {
3 send(c.port, { kind: 'broadcast', channel, payload, from });
4 }
5}
broadcast
menghantar mesej pada saluran yang ditetapkan kepada semua klien.
1function handleRequest(clientId: string, port: MessagePort, req: RequestMessage) {
handleRequest
memproses permintaan masuk mengikut jenis dan memulangkan hasilnya kepada klien.
1 const { action } = req;
2 try {
3 switch (action.type) {
4 case 'ping':
5 send(port, respond(req, true, 'pong'));
6 break;
7 case 'echo':
8 send(port, respond(req, true, action.payload));
9 break;
10 case 'getTime':
11 send(port, respond(req, true, new Date().toISOString()));
12 break;
13 case 'set':
14 kv.set(action.key, action.value);
15 send(port, respond(req, true, true));
16 break;
17 case 'get':
18 send(port, respond(req, true, kv.get(action.key)));
19 break;
20 case 'broadcast':
21 broadcast(clientId, action.channel, action.payload);
22 send(port, respond(req, true, true));
23 break;
- Dalam kod ini, bergantung pada jenis permintaan yang diterima, ia mengendalikan penghantaran dan penerimaan mesej, mendapatkan dan menyimpan data, serta penyiaran.
1 case 'lock.acquire': {
2 const owner = locks.get(action.key);
3 if (!owner) {
4 locks.set(action.key, clientId);
5 send(port, respond(req, true, { owner: clientId }));
6 } else if (owner === clientId) {
7 send(port, respond(req, true, { owner }));
8 } else {
9 const start = Date.now();
10 const tryWait = () => {
11 const current = locks.get(action.key);
12 if (!current) {
13 locks.set(action.key, clientId);
14 send(port, respond(req, true, { owner: clientId }));
15 } else if ((action.timeoutMs ?? 5000) < Date.now() - start) {
16 send(port, respond(req, false, undefined, 'lock-timeout'));
17 } else {
18 setTimeout(tryWait, 25);
19 }
20 };
21 tryWait();
22 }
23 break;
24 }
- Kod ini melaksanakan proses untuk klien memperoleh kuncian bagi kekunci yang dinyatakan. Jika kuncian belum dipegang, ia diperoleh serta-merta; jika klien yang sama memintanya lagi, permintaan itu juga dianggap berjaya. Jika klien lain sudah memegang kuncian tersebut, ia akan mencuba semula setiap 25 milisaat sehingga kuncian dilepaskan, dan jika had masa (lalai 5 saat) yang ditetapkan terlampaui, ia akan membalas dengan ralat.
1 case 'lock.release':
2 if (locks.get(action.key) === clientId) {
3 locks.delete(action.key);
4 send(port, respond(req, true, true));
5 } else {
6 send(port, respond(req, false, undefined, 'not-owner'));
7 }
8 break;
9 default:
10 send(port, respond(req, false, undefined, 'unknown-action'));
11 }
12 } catch (e: any) {
13 send(port, respond(req, false, undefined, e?.message ?? 'error'));
14 }
15}
- Kod ini melepaskan lock yang dipegang oleh klien dan memulangkan respons ralat jika klien tiada kebenaran atau tindakan tersebut tidak diketahui.
1function handleInMessage(c: Client, msg: WorkerInMessage) {
2 if (msg.kind === 'heartbeat') {
3 c.lastBeat = Date.now();
4 } else if (msg.kind === 'bye') {
5 cleanupClient(msg.from);
6 } else if (msg.kind === 'request') {
7 handleRequest(c.id, c.port, msg);
8 }
9}
handleInMessage
menghuraikan mesej yang diterima daripada klien serta mengendalikan permintaan dan heartbeat.
1function cleanupClient(clientId: string) {
2 for (const [key, owner] of locks) {
3 if (owner === clientId) locks.delete(key);
4 }
5 clients.delete(clientId);
6}
cleanupClient
mengalih keluar klien yang terputus sambungan daripada daftar dan keadaan kunci.
1setInterval(() => {
2 const now = Date.now();
3 for (const [id, c] of clients) {
4 if (now - c.lastBeat > HEARTBEAT_TIMEOUT) cleanupClient(id);
5 }
6}, 10_000);
- Menggunakan
setInterval
untuk menyemak heartbeat semua klien secara berkala dan membersihkan sambungan yang telah tamat masa.
1self.onconnect = (e: MessageEvent) => {
2 const port = (e.ports && e.ports[0]) as MessagePort;
3 const clientId = crypto.randomUUID?.() ?? Math.random().toString(36).slice(2);
4 const client: Client = { id: clientId, port, lastBeat: Date.now() };
5 clients.set(clientId, client);
6
7 port.addEventListener('message', (ev) => handleInMessage(client, ev.data as WorkerInMessage));
8 send(port, { kind: 'broadcast', channel: 'system', payload: { hello: true, clientId }, from: 'worker' });
9 port.start();
10};
-
onconnect
dipanggil apabila tab atau halaman baharu bersambung kepadaShared Worker
, mendaftarkan klien dan memulakan komunikasi. -
Di seluruh fail ini, mekanisme asas
Shared Worker
yang membolehkan pengurusan keadaan bersama dan komunikasi merentas berbilang tab pelayar telah dilaksanakan.
Pembungkus klien (RPC)
Seterusnya, cipta klien RPC berasaskan Promise.
1// shared-worker-client.ts
2import {
3 RequestAction,
4 RequestMessage,
5 WorkerOutMessage,
6 randomId
7} from './worker-protocol.js';
- Mengimport definisi jenis dan fungsi utiliti yang digunakan untuk komunikasi worker.
1export type BroadcastHandler = (msg: {
2 channel: string;
3 payload: unknown;
4 from: string
5}) => void;
- Di sini kita mentakrif jenis fungsi panggil balik (callback) yang dijalankan apabila mesej siaran diterima.
1export class SharedWorkerClient {
SharedWorkerClient
ialah kelas klien yang berkomunikasi denganShared Worker
, menghantar permintaan dan mengendalikan respons.
1 private worker: SharedWorker;
2 private port: MessagePort;
3 private pending = new Map<string, {
4 resolve: (v: any) => void;
5 reject: (e: any) => void
6 }>();
- Pembolehubah ini ialah instans worker, port komunikasi dengan worker, dan peta yang menjejak permintaan yang menunggu respons.
1 private clientId = randomId();
2 private heartbeatTimer?: number;
3 private onBroadcast?: BroadcastHandler;
- Pembolehubah ini menyimpan pengecam klien, pemasa untuk menghantar heartbeat, dan pengendali penerimaan siaran.
1 constructor(url: URL, name = 'app-shared', onBroadcast?: BroadcastHandler) {
2 this.worker = new SharedWorker(url, { name, type: 'module' as any });
3 this.port = this.worker.port;
4 this.onBroadcast = onBroadcast;
- Dalam pembina (constructor), ia memulakan sambungan ke
Shared Worker
dan menyediakan pendengar mesej serta penghantaran heartbeat.
1 this.port.addEventListener('message', (ev) => {
2 const msg = ev.data as WorkerOutMessage;
3 if (msg.kind === 'response') {
4 const p = this.pending.get(msg.id);
5 if (p) {
6 this.pending.delete(msg.id);
7 msg.ok ? p.resolve(msg.result) : p.reject(new Error(msg.error || 'error'));
8 }
9 } else if (msg.kind === 'broadcast') {
10 this.onBroadcast?.(msg);
11 }
12 });
13 this.port.start();
- Di sini ia menerima mesej daripada worker dan mengendalikan respons atau siaran.
1 this.heartbeatTimer = window.setInterval(() => {
2 this.port.postMessage({ kind: 'heartbeat', from: this.clientId });
3 }, 10_000);
- Menghantar mesej heartbeat secara berkala untuk mengekalkan sambungan hidup.
1 window.addEventListener('beforeunload', () => {
2 try {
3 this.port.postMessage({ kind: 'bye', from: this.clientId });
4 } catch {}
5 if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
6 });
7 }
- Menghantar pemberitahuan pemutusan sambungan kepada worker sebelum tetingkap ditutup.
1 request<T = unknown>(action: RequestAction): Promise<T> {
2 const id = randomId();
3 return new Promise<T>((resolve, reject) => {
4 this.pending.set(id, { resolve, reject });
5 this.port.postMessage({ kind: 'request', id, from: this.clientId, action });
6 });
7 }
- Kaedah
request
menghantar tindakan yang dinyatakan kepada worker dan menerima keputusan sebagaiPromise
.
1 ping() {
2 return this.request<string>({ type: 'ping' });
3 }
4 echo(payload: string) {
5 return this.request<string>({ type: 'echo', payload });
6 }
7 getTime() {
8 return this.request<string>({ type: 'getTime' });
9 }
- Ini ialah kaedah utiliti untuk ujian komunikasi asas dan mendapatkan masa semasa.
1 set(key: string, value: unknown) {
2 return this.request<boolean>({ type: 'set', key, value });
3 }
4 get<T = unknown>(key: string) {
5 return this.request<T>({ type: 'get', key });
6 }
- Ini ialah kaedah untuk menyimpan dan mendapatkan pasangan kunci-nilai.
1 broadcast(channel: string, payload: unknown) {
2 return this.request<boolean>({ type: 'broadcast', channel, payload });
3 }
- Ini ialah kaedah yang menghantar mesej siaran kepada klien lain melalui worker.
1 lockAcquire(key: string, timeoutMs?: number) {
2 return this.request<{ owner: string }>({ type: 'lock.acquire', key, timeoutMs });
3 }
4 lockRelease(key: string) {
5 return this.request<boolean>({ type: 'lock.release', key });
6 }
7}
- Ini ialah kaedah yang memperoleh dan melepaskan kunci (lock) untuk mencapai pengecualian bersama ke atas sumber yang dikongsi.
- Di seluruh fail ini, API klien dilaksanakan untuk komunikasi tidak segerak yang selamat dari setiap tab pelayar ke
Shared Worker
.
Contoh penggunaan
Dalam demo.ts
, kami menggunakan kelas SharedWorkerClient
yang dicipta sebelum ini dan mengesahkan tingkah lakunya. Ia melaksanakan secara berurutan satu siri fungsi termasuk ujian komunikasi, membaca dan menulis data, penyiaran, serta pengendalian lock.
1// demo.ts
2import { SharedWorkerClient } from './shared-worker-client.js';
3
4const client = new SharedWorkerClient(new URL('./shared-worker.js', import.meta.url), 'app-shared', (b) => {
5 console.log('[BROADCAST]', b.channel, JSON.stringify(b.payload));
6});
7
8async function demo() {
9 console.log('ping:', await client.ping());
10 console.log('echo:', await client.echo('hello'));
11 console.log('time:', await client.getTime());
12
13 // Counter update
14 await client.set('counter', (await client.get<number>('counter')) ?? 0);
15 const c1 = await client.get<number>('counter');
16 await client.set('counter', (c1 ?? 0) + 1);
17 console.log('counter:', await client.get<number>('counter'));
18
19 // Broadcast test
20 await client.broadcast('notify', { msg: 'Counter updated' });
21
22 // Lock test
23 const key = 'critical';
24 console.log(`[lock] Trying to acquire lock: "${key}"`);
25 const lockResult = await client.lockAcquire(key, 2000);
26 console.log(`[lock] Lock acquired for key "${key}":`, lockResult);
27
28 try {
29 console.log(`[lock] Simulating critical section for key "${key}"...`);
30 await new Promise((r) => setTimeout(r, 250));
31 console.log(`[lock] Critical section completed for key "${key}"`);
32 } finally {
33 console.log(`[lock] Releasing lock: "${key}"`);
34 const releaseResult = await client.lockRelease(key);
35 console.log(`[lock] Lock released for key "${key}":`, releaseResult);
36 }
37}
38
39demo().catch(console.error);
- Kod ini ialah demo yang menggunakan
Shared Worker
untuk berkongsi dan menyegerakkan data serta keadaan merentas berbilang tab pelayar. Dengan menggunakan komunikasi berasaskan mesej, anda boleh bertukar mesej tidak segerak dengan selamat dan gandingan longgar, menjadikannya lebih mudah untuk mengurus komunikasi antara konteks yang berbeza. Selain itu, dengan menggunakan RPC, ia mengabstrakkan komunikasi dengan worker dalam gaya yang intuitif seperti panggilan kaedah, sekali gus meningkatkan kebolehselenggaraan dan kebolehbacaan.
Ujian dalam HTML
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8" />
5 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6 <title>Shared Worker Demo</title>
7</head>
8<body>
9 <h1>SharedWorker Demo</h1>
10 <p>Open the browser console to see the output.</p>
11
12 <script type="module">
13 import './demo.js';
14 </script>
15</body>
16</html>
Pertimbangan reka bentuk dan operasi
Apabila mereka bentuk dan mengendalikan, mengambil kira perkara berikut akan membantu anda membina sistem yang lebih mantap dan boleh diperluas.
- Anda boleh mengguna pakai kesatuan terdiskriminasi (tagged union) yang membolehkan percabangan berdasarkan
kind
atautype
. - Gunakan correlation ID untuk memadankan permintaan dengan respons dengan betul.
- Heartbeat dan pembersihan automatik boleh mengelakkan kunci yang ditinggalkan.
- Laksanakan versioning untuk menampung perubahan protokol pada masa hadapan dengan fleksibel.
- Mentakrifkan kod ralat yang jelas memudahkan pengendalian di sisi UI dan penyahpepijatan.
Ringkasan
Shared Worker
ialah mekanisme teras untuk berkongsi data dan keadaan merentas berbilang tab pelayar.
Struktur yang diperkenalkan di sini menyediakan komunikasi RPC yang type-safe, pemantauan keterhidupan melalui heartbeat, serta mekanisme penguncian, menjadikannya reka bentuk yang teguh dan boleh digunakan terus dalam produksi.
Berdasarkan mekanisme ini, anda juga boleh melaksanakan aplikasi berikut.
- Menjadikan akses IndexedDB secara bersiri
- Penyepaduan dan perkongsian sambungan WebSocket
- Membina baris gilir kerja merentasi berbilang tab
- Pendikitan (throttling) dan penyampaian pemberitahuan kemajuan
Seperti yang anda lihat, memanfaatkan Shared Worker
membolehkan perkongsian data dan pemprosesan dengan selamat dan cekap merentas berbilang tab.
Anda boleh mengikuti artikel di atas menggunakan Visual Studio Code di saluran YouTube kami. Sila lihat juga saluran YouTube kami.