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 & 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 deService Worker
.Service Worker
agit principalement comme un proxy réseau, tandis queShared 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
, unMessagePort
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 gestionnaireonclose
standard. Côté principal, envoyez un message{type: 'bye'}
avecport.postMessage
pendant l’événementbeforeunload
, 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 », unService Worker
pour le « contrôle réseau », et unDedicated 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 utilisezport.addEventListener('message', ...)
, vous devez appelerport.start()
. Ceci est inutile si vous utilisezport.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 exempleArrayBuffer
), ou d’utiliser une mémoire partagée (SharedArrayBuffer
) en combinaison avecAtomics
. -
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.