`Shared Worker` dalam TypeScript

`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 seperti ping, get dan broadcast.
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 bagi Shared 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 kepada Shared 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 dengan Shared 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 sebagai Promise.
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 atau type.
  • 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.

YouTube Video