Shared Worker en JavaScript

Shared Worker en JavaScript

Cet article explique le Shared Worker en JavaScript.

Nous expliquerons tout, des bases du Shared Worker aux cas d’utilisation concrets, étape par étape.

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

Qu'est-ce qu'un Shared Worker ?

Un Shared Worker est un thread de travail qui peut être partagé entre plusieurs pages (onglets, fenêtres, iframes, etc.) du même origine. Contrairement à un Dedicated Worker spécifique à une page, la principale caractéristique est qu’un seul processus d’arrière-plan peut être partagé entre plusieurs pages. Les cas d’utilisation typiques incluent les suivants :.

  • Vous pouvez partager une seule connexion WebSocket entre plusieurs onglets, ce qui réduit le nombre de connexions et centralise les reconnexions.
  • Vous pouvez synchroniser l'état entre plusieurs onglets (gestion centralisée pour Pub/Sub ou magasins de données).
  • Vous pouvez sérialiser les opérations IndexedDB, en médiant l'accès simultané.
  • Vous pouvez éviter l’exécution doublée de processus coûteux en calcul.

Shared Worker a un rôle différent de Service Worker. Service Worker agit principalement comme un proxy réseau, tandis que Shared Worker se concentre sur l’exécution de calculs arbitraires ou la gestion d’état partagée entre plusieurs pages.

API de base et cycle de vie

Création (Thread principal)

 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 });
  • Ce code montre comment créer un Shared Worker et envoyer/recevoir des messages via son port.

Récepteur (dans la portée du 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)
  • Ce code montre comment, à l’intérieur du Shared Worker, un MessagePort est reçu pour chaque connexion et comment traiter et répondre aux messages des clients.

Cycle de vie d'un Shared Worker

Le Shared Worker démarre lorsque la première connexion est établie et peut se terminer lorsque le dernier port est fermé. Les connexions se ferment lors du rechargement/fermeture de la page, et le worker est recréé si nécessaire.

Un Shared Worker est un « script de longue durée partagé dans le navigateur ». Si vous ne faites pas attention au cycle de vie du Shared Worker, des problèmes tels que des fuites de ressources, la persistance d’un état obsolète et des redémarrages inattendus risquent de se produire.

Essayez : Diffusion entre onglets

Ceci est une implémentation minimale où plusieurs onglets se connectent au même Shared Worker et tout message envoyé est diffusé à tous les onglets.

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});
  • Ce code se connecte à un Shared Worker, implémente la réception/affichage de messages, l'envoi de messages via un formulaire, et l’envoi d’un ping en cliquant sur un bouton.

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};
  • Ce code implémente une fonction pub/sub simple à l’intérieur du Shared Worker pour relayer des messages entre plusieurs clients.

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>
  • Si vous ouvrez cette page dans plusieurs onglets, les messages envoyés depuis un onglet seront reçus par les autres.

Conception des messages : Request/Response et Correlation ID

Lorsque plusieurs clients interagissent avec un Shared Worker, il est souvent nécessaire d’identifier quelle réponse correspond à quelle requête. Par conséquent, il est d’usage d’inclure un identifiant de corrélation afin d’associer les réponses aux requêtes.

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'
  • Ce code implémente un client RPC simple capable d’effectuer des appels de méthodes asynchrones au Shared Worker. Ici, un serveur RPC (Remote Procedure Call) désigne un serveur qui fournit un mécanisme permettant d’appeler des fonctions et des procédures à partir d’autres programmes ou processus.
  • Dans cet exemple, l’id est simplement incrémenté, mais vous pouvez également utiliser un UUID, une chaîne aléatoire ou combiner un horodatage avec un compteur pour créer une clé unique.

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};
  • Ce code implémente un serveur RPC simple qui exécute le traitement selon la method et renvoie le résultat accompagné de l’ID de la requête.

Schémas d’utilisation typiques

Il existe plusieurs schémas pratiques lors de l’utilisation d’un Shared Worker, par exemple :.

Multiplexage d’un seul WebSocket (partagé entre plusieurs onglets)

Si chaque onglet ouvre sa propre connexion WebSocket, cela affectera la charge du serveur et la limite de connexions. Placez un seul WebSocket dans le Shared Worker et chaque onglet envoie/reçoit des messages via le 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}
  • Ce code implémente la gestion de l’envoi/réception de messages à un serveur WebSocket et la réception d’états de connexion via le 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};
  • Ce code implémente un mécanisme dans le Shared Worker pour partager une seule connexion WebSocket, diffuser l’état de connexion et les données reçues à tous les clients, tout en envoyant les requêtes de chaque client au serveur via ce WebSocket.

  • En centralisant les stratégies de reconnexion et la mise en file d’attente des messages non envoyés dans le Shared Worker, le comportement devient cohérent entre tous les onglets.

Médiation IndexedDB (sérialisation)

Lors d’un accès à la même base de données depuis plusieurs onglets, pour éviter les conflits lors de transactions simultanées ou l’attente de verrous, vous pouvez les mettre en file dans le Shared Worker et les traiter séquentiellement.

 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};
  • Ce code implémente un mécanisme où les opérations de lecture/écriture sur IndexedDB sont mises en file via le Shared Worker, sérialisant l’accès depuis plusieurs onglets pour éviter les conflits et la contention des verrous.

Modularisation et conseils sur les bundlers

Si vous souhaitez écrire votre script Shared Worker comme un module ES, le comportement et la prise en charge peuvent varier selon l’environnement. En pratique, il est plus sûr de choisir l’une des options suivantes :.

  • Écrivez en format worker classique et utilisez importScripts() pour charger les dépendances si nécessaire.
  • Utilisez la fonction d’entrée worker de votre bundler (comme Vite, Webpack, esbuild, etc.), et créez un bundle séparé pour le Shared Worker au moment du build.

Astuces pour la gestion des erreurs, la détection de déconnexion et la robustesse

Considérez les points suivants pour la gestion des erreurs et la robustesse :.

  • Gérer l’envoi de messages avant l’établissement de la connexion Mettez en file les messages reçus avant que le port soit prêt.

  • Détection de la déconnexion MessagePort n’a pas de gestionnaire onclose standard. Côté principal, envoyez un message {type: 'bye'} avec port.postMessage pendant l’événement beforeunload, ou spécifiez-le clairement dans le protocole pour garantir que le worker soit correctement nettoyé.

  • Reconnexion Lorsque la page est rechargée ou qu’un onglet est rouvert, un nouveau port est créé. Préparez un message d’initialisation de synchronisation (pour transmettre tout l’état d’un coup).

  • Contre-pression En cas de diffusion intensive, utilisez des techniques de throttling/debouncing ou envoyez des instantanés.

  • Sécurité Un Shared Worker est fondamentalement partagé à l’intérieur du même origine. Si vous placez des secrets dans le worker, pensez à concevoir une vérification latérale avec des jetons ou un procédé similaire pour le côté appelant.

Comment utiliser correctement un Dedicated Worker, un Shared Worker et un Service Worker

Chaque type de Worker possède les caractéristiques suivantes :.

  • Dedicated Worker Dedicated Worker est destiné à être utilisé uniquement par une seule page. Il permet de séparer les calculs de l’interface utilisateur selon un schéma 1:1.

  • Shared Worker Shared Worker peut être partagé entre plusieurs pages ayant la même origine. Il est idéal pour la communication entre onglets et le partage d’une connexion unique.

  • Service Worker Service Worker peut être utilisé pour le proxy réseau, la mise en cache, des opérations hors ligne, les notifications push et la synchronisation en arrière-plan. Sa force réside dans la capacité à intercepter les requêtes fetch.

En règle générale : utilisez un Shared Worker pour le « partage d’informations et le traitement arbitraire entre onglets », un Service Worker pour le « contrôle réseau », et un Dedicated Worker si vous souhaitez juste décharger des traitements lourds de l’UI.

Pièges courants

Lorsque vous utilisez un Shared Worker, faites attention aux points suivants.

  • Oublier d’appeler start() ou l’invoquer inutilement Lorsque vous utilisez port.addEventListener('message', ...), vous devez appeler port.start(). Ceci est inutile si vous utilisez port.onmessage = ....

  • Diffusion sans restriction La charge augmente avec le nombre d’onglets. Vous pouvez réduire la charge en mettant en place une diffusion différenciée ou des sujets d’abonnement (filtrage par sujet).

  • Coût de copie des objets postMessage duplique les données. Pour de grandes quantités de données, envisagez de les envoyer comme Transferable (par exemple ArrayBuffer), ou d’utiliser une mémoire partagée (SharedArrayBuffer) en combinaison avec Atomics.

  • Durée de vie et réinitialisation Le Shared Worker peut se terminer lorsque le dernier client se déconnecte. Concevez soigneusement l’initialisation pour les premières connexions et relances afin d’éviter les effets de bord et bugs.

Résumé

  • Le Shared Worker sert à implémenter une logique personnalisée à longue durée de vie partagée entre plusieurs pages.
  • Clarifiez le contrat de messages (types/protocoles) et rendez votre conception requête/réponse robuste en ajoutant des identifiants de corrélation.
  • Il est idéal pour les processus qui fonctionnent mieux lorsqu’ils sont unifiés sur tous les onglets, par exemple le multiplexage des connexions WebSocket ou la sérialisation de l’accès à IndexedDB.
  • Le réglage précis de détails tels que les bundlers, les définitions de types et la reconnexion peut grandement améliorer la maintenabilité et l’expérience utilisateur.

Vous pouvez suivre l'article ci-dessus avec Visual Studio Code sur notre chaîne YouTube. Veuillez également consulter la chaîne YouTube.

YouTube Video