`Shared Worker` i TypeScript
Den här artikeln förklarar Shared Worker
i TypeScript.
Vi förklarar i detalj hur Shared Workers fungerar och hur man använder dem i praktiken, med TypeScript-exempel.
YouTube Video
Shared Worker
i TypeScript
Shared Worker
är en enda worker-process som delas mellan flera flikar, fönster och iframes på samma ursprung. Med detta kan du hantera delat tillstånd och resurser över flera webbläsarflikar.
Till exempel kan du effektivt implementera en delad WebSocket-anslutning, cache- och köhantering synkroniserad mellan flikar samt ömsesidig uteslutning (mutex).
Till skillnad från en Dedicated Worker
tar en Shared Worker
emot flera MessagePort
via onconnect
-händelsen och kan multiplexa kommunikationen med flera klienter.
Fall där du bör välja en Shared Worker
I följande fall är det lämpligt att använda en Shared Worker
.
- När du behöver delat tillstånd eller ömsesidig uteslutning mellan flikar
- När du vill dela en enda WebSocket-anslutning eller åtkomst till IndexedDB
- När du behöver notifiera alla flikar (broadcast)
- När du vill centralisera tung bearbetning för att spara resurser
Omvänt är andra angreppssätt mer lämpliga i följande fall.
- När du behöver cachekontroll eller offline-stöd kan du använda en
Service Worker
. - För tung bearbetning som är begränsad till en enda flik kan du använda en
Dedicated Worker
.
Implementeringssteg för Shared Worker
Här implementerar vi följande steg för steg med TypeScript.
- Typsäkert meddelandeprotokoll
- Promise-baserad begäran/svar (RPC)
- Utsändning till alla flikar
- Hjärtslag och rensning av klienter
Konfigurera miljön
Skapa konfigurationen för att kompilera varje källfil som använder en 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}
- I
tsconfig-src.json
, aktivera DOM- och Web Worker-typdefinitioner så att koden kan kompileras säkert.
Definiera meddelandeprotokollet
Grunden för kommunikationen är ett typat meddelandekontrakt. Om du definierar detta i förväg blir efterföljande kommunikation säker och lätt att utöka.
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
är en diskriminerad union (taggad union) som representerar typer av förfrågningar som skickas till workern och definierar operationer somping
,get
ochbroadcast
.
1export interface RequestMessage {
2 kind: 'request';
3 id: string;
4 from: string;
5 action: RequestAction;
6}
RequestMessage
definierar strukturen för begärandemeddelanden som skickas från klienten till workern.
1export interface ResponseMessage {
2 kind: 'response';
3 id: string;
4 ok: boolean;
5 result?: unknown;
6 error?: string;
7}
ResponseMessage
definierar strukturen för svarsmeddelanden som returneras från workern till klienten.
1export interface BroadcastMessage {
2 kind: 'broadcast';
3 channel: string;
4 payload: unknown;
5 from: string;
6}
BroadcastMessage
definierar strukturen för broadcast-meddelanden som workern skickar till andra klienter.
1export type WorkerInMessage =
2 | RequestMessage
3 | { kind: 'heartbeat'; from: string }
4 | { kind: 'bye'; from: string };
WorkerInMessage
är en typ som representerar alla meddelanden som workern tar emot, såsom begäranden, heartbeat-signaler och frånkopplingsnotiser.
1export type WorkerOutMessage = ResponseMessage | BroadcastMessage;
WorkerOutMessage
är en typ som representerar svars- eller broadcast-meddelanden som workern skickar till klienten.
1export const randomId = () => Math.random().toString(36).slice(2);
randomId
är en funktion som genererar en slumpmässig alfanumerisk sträng för meddelande-ID och liknande.
Implementera Shared Worker
I shared-worker.ts
registrerar du flikar som ansluter via onconnect
-händelsen och hanterar meddelanden.
1// shared-worker.ts
2/// <reference lib="webworker" />
- Detta direktiv instruerar TypeScript att läsa in typdefinitioner för Web Workers.
1import {
2 WorkerInMessage,
3 WorkerOutMessage,
4 RequestMessage,
5 ResponseMessage,
6} from './worker-protocol.js';
- Importerar typdefinitionerna som används för kommunikation med workern.
1export default {};
2declare const self: SharedWorkerGlobalScope;
- Deklarerar uttryckligen att
self
är det globala omfånget förShared Worker
.
1type Client = {
2 id: string;
3 port: MessagePort;
4 lastBeat: number;
5};
Client
är en typ som representerar varje klients identifierare, kommunikationsport och senaste heartbeat-tidsstämpel.
1const clients = new Map<string, Client>();
2const kv = new Map<string, unknown>();
3const locks = new Map<string, string>();
4const HEARTBEAT_TIMEOUT = 30_000;
- Hanterar listan över anslutna klienter, ett nyckel-värde-lager, låstillstånd och timeout-tider.
1function send(port: MessagePort, msg: WorkerOutMessage) {
2 port.postMessage(msg);
3}
send
är en hjälpfunktion som skickar ett meddelande till den angivna porten.
1function respond(req: RequestMessage, ok: boolean, result?: unknown, error?: string): ResponseMessage {
2 return { kind: 'response', id: req.id, ok, result, error };
3}
respond
genererar ett svarsmeddelande för en begäran.
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
skickar ett meddelande på en angiven kanal till alla klienter.
1function handleRequest(clientId: string, port: MessagePort, req: RequestMessage) {
handleRequest
bearbetar inkommande begäranden efter typ och returnerar resultaten till klienten.
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;
- I den här koden hanteras, beroende på vilken typ av begäran som tas emot, skickande och mottagande av meddelanden, hämtning och sparande av data samt utsändning (broadcast).
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 }
- Den här koden implementerar processen för att en klient ska kunna ta ett lås för den angivna nyckeln. Om låset inte redan är taget tas det omedelbart; om samma klient begär det igen betraktas begäran också som lyckad. Om en annan klient redan har låset görs nya försök var 25:e millisekund tills låset släpps, och om den angivna tidsgränsen (standard 5 sekunder) överskrids returneras ett fel.
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}
- Den här koden frigör låset som hålls av klienten och returnerar ett felsvar om klienten saknar behörighet eller åtgärden är okänd.
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
tolkar meddelanden som tas emot från klienter och hanterar begäranden och heartbeat-signaler.
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
tar bort frånkopplade klienter från registret och låstillståndet.
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);
- Använder
setInterval
för att periodiskt kontrollera alla klienters heartbeat-signaler och städa upp anslutningar som har gått i timeout.
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
anropas när en ny flik eller sida ansluter tillShared Worker
, registrerar klienten och startar kommunikationen. -
I den här filen implementeras de grundläggande mekanismerna i en
Shared Worker
som möjliggör delad tillståndshantering och kommunikation mellan flera webbläsarflikar.
Klientomslag (RPC)
Skapa sedan en Promise-baserad RPC-klient.
1// shared-worker-client.ts
2import {
3 RequestAction,
4 RequestMessage,
5 WorkerOutMessage,
6 randomId
7} from './worker-protocol.js';
- Importerar typdefinitionerna och hjälpfunktionerna som används för kommunikation med workern.
1export type BroadcastHandler = (msg: {
2 channel: string;
3 payload: unknown;
4 from: string
5}) => void;
- Här definierar vi typen för återanropsfunktionen som körs när ett broadcast-meddelande tas emot.
1export class SharedWorkerClient {
SharedWorkerClient
är en klientklass som kommunicerar med enShared Worker
, skickar begäranden och hanterar svar.
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 }>();
- Dessa variabler är worker-instansen, kommunikationsporten mot workern och en map som spårar begäranden som väntar på svar.
1 private clientId = randomId();
2 private heartbeatTimer?: number;
3 private onBroadcast?: BroadcastHandler;
- Dessa variabler håller klientens identifierare, timern för att skicka heartbeats och hanteraren för mottagning av broadcast.
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;
- I konstruktorn initierar den anslutningen till
Shared Worker
och sätter upp meddelandelyssnare och heartbeat-sändning.
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();
- Här tar den emot meddelanden från workern och hanterar svar eller broadcast-meddelanden.
1 this.heartbeatTimer = window.setInterval(() => {
2 this.port.postMessage({ kind: 'heartbeat', from: this.clientId });
3 }, 10_000);
- Skickar heartbeat-meddelanden periodiskt för att hålla anslutningen vid liv.
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 }
- Skickar en frånkopplingsnotis till workern innan fönstret stängs.
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 }
- Metoden
request
skickar den angivna åtgärden till workern och tar emot resultatet som ettPromise
.
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 }
- Detta är hjälpmetoder för grundläggande kommunikationstester och hämtning av aktuell tid.
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 }
- Detta är metoder för att lagra och hämta nyckel-värde-par.
1 broadcast(channel: string, payload: unknown) {
2 return this.request<boolean>({ type: 'broadcast', channel, payload });
3 }
- Detta är en metod som skickar broadcast-meddelanden till andra klienter via workern.
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}
- Detta är metoder som tar och släpper lås för att uppnå ömsesidig uteslutning över delade resurser.
- I den här filen implementeras ett klient-API för säker, asynkron kommunikation från varje webbläsarflik till Shared Workern.
Exempel på användning
I demo.ts
använder vi klassen SharedWorkerClient
som skapades tidigare och verifierar dess beteende. Den kör sekventiellt en serie funktioner, inklusive kommunikationstester, läsning och skrivning av data, utsändning (broadcast) samt låshantering.
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);
- Den här koden är en demo som använder en
Shared Worker
för att dela och synkronisera data och tillstånd mellan flera webbläsarflikar. Genom att använda meddelandebaserad kommunikation kan du på ett säkert sätt utbyta asynkrona meddelanden med lös koppling, vilket gör det lättare att hantera kommunikationen mellan olika kontexter. Dessutom abstraherar användningen av RPC kommunikationen med workern på ett intuitivt, metodanropsliknande sätt, vilket förbättrar underhållbarhet och läsbarhet.
Testa i 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>
Design- och driftöverväganden
Vid utformning och drift hjälper följande punkter dig att bygga ett mer robust och utökningsbart system.
- Du kan använda en diskriminerad union (taggad union) som gör att du kan grena på
kind
ellertype
. - Använd ett korrelations-ID för att korrekt matcha begäranden med svar.
- Heartbeats och automatisk upprensning kan förhindra övergivna lås.
- Implementera versionshantering för att flexibelt kunna hantera framtida protokolländringar.
- Att definiera tydliga felkoder gör hantering och felsökning på UI-sidan enklare.
Sammanfattning
En Shared Worker
är en kärnmekanism för att dela data och tillstånd mellan flera webbläsarflikar.
Strukturen som introduceras här ger typsäker RPC-kommunikation, övervakning av aktivitet via heartbeats och en låsmekanism, vilket gör den till en robust design som kan användas som den är i produktion.
Ovanpå denna mekanism kan du också implementera följande tillämpningar.
- Serialisera åtkomst till IndexedDB
- Integrering och delning av WebSocket-anslutningar
- Att bygga en jobbkö över flera flikar
- Throttling och leverans av förloppsnotiser
Som du ser gör användningen av en Shared Worker
det möjligt att dela data och bearbetning säkert och effektivt över flera flikar.
Du kan följa med i artikeln ovan med hjälp av Visual Studio Code på vår YouTube-kanal. Vänligen kolla även in YouTube-kanalen.