Cours JavaScript et TypeScript · Module 8: Événements et DOM
Event Bus et architecture applicative
L'implémentation d'un système de communication entre composants à l'aide d'un event bus.
Imagine un module qui détecte les évasions de dinosaures. Quand un raptor force la clôture, il faut déclencher la sirène, marquer la zone sur la carte, verrouiller les portails et envoyer un message à la sécurité. Si le module des évasions appelle directement chacune de ces fonctions, chaque nouveau système oblige à fouiller dans son code, et au bout d'un an, plus personne ne sait ce qui dépend de quoi. Un Event Bus fonctionne comme le central radio du parc : les modules diffusent des messages sur un bus commun, et ceux que ça intéresse les écoutent d'eux-mêmes. Personne n'a besoin de connaître l'adresse de personne.
Implémentation de l'Event Bus
La classe conserve les écouteurs dans l'objet events. La clé est le nom de l'événement, et la valeur, un tableau des fonctions qui l'attendent.
1class EventBus {
2 constructor() {
3 this.events = {};
4 }
5
6 // Enregistrement d'un écouteur
7 on(eventName, callback) {
8 if (!this.events[eventName]) {
9 this.events[eventName] = [];
10 }
11 this.events[eventName].push(callback);
12
13 // Renvoyer une fonction qui supprime l'écouteur
14 return () => this.off(eventName, callback);
15 }La méthode on() crée le tableau lors du premier enregistrement, y ajoute la fonction et renvoie une fonction de désinscription toute prête. Grâce à cela, un module n'a pas besoin de conserver une référence à son callback uniquement pour pouvoir le supprimer plus tard. L'écriture eventBus.on('alert', callback) se lit comme une phrase : écoute alert, et à ce moment-là, appelle le callback. Les deux méthodes suivantes retirent des écouteurs.
1 // Suppression d'un écouteur
2 off(eventName, callback) {
3 if (this.events[eventName]) {
4 this.events[eventName] = this.events[eventName].filter(cb => cb !== callback);
5 }
6 }
7
8 // Écouteur à usage unique
9 once(eventName, callback) {
10 const onceCallback = (...args) => {
11 callback(...args);
12 this.off(eventName, onceCallback);
13 };
14 this.on(eventName, onceCallback);
15 }La méthode off() ne modifie pas l'ancien tableau, elle en crée un nouveau avec filter(), donc supprimer un écouteur pendant une émission ne casse pas la boucle en cours. La méthode once() enveloppe le callback dans onceCallback, qui appelle d'abord l'original, puis se désinscrit lui-même. L'écouteur agit donc exactement une fois et disparaît automatiquement. Cette version a une limite : on a enregistré onceCallback, et non le callback d'origine, donc un appel à off() avec l'original ne le supprimera pas. Place à la diffusion des messages.
1 // Émission d'un événement
2 emit(eventName, ...args) {
3 if (this.events[eventName]) {
4 this.events[eventName].forEach(callback => {
5 try {
6 callback(...args);
7 } catch (error) {
8 console.error(`Error in event listener for ${eventName}:`, error);
9 }
10 });
11 }
12 }La méthode emit() transmet tous les arguments à chaque écouteur, dans l'ordre d'enregistrement. Le bloc try...catch autour de chaque appel est une décision d'architecture importante : une erreur dans un module arrive dans la console, mais n'arrête pas les notifications destinées aux autres. La sirène retentira même si le module de la carte est justement en panne. Pour finir, le nettoyage en gros.
1 // Suppression de tous les écouteurs d'un événement
2 removeAllListeners(eventName) {
3 if (eventName) {
4 delete this.events[eventName];
5 } else {
6 this.events = {};
7 }
8 }
9}La méthode removeAllListeners() avec un nom supprime un seul canal, et sans argument, elle vide tout le bus. C'est utile à la fermeture de l'application et dans les tests, quand chaque test doit démarrer avec un central propre.
Utilisation dans les modules de l'application
Dans l'application, on crée une seule instance partagée, que tous les modules importent. Le module A s'abonne au message d'évasion.
1// Instance globale de l'event bus
2const eventBus = new EventBus();
3
4// Utilisation dans différents modules de l'application
5// Module A
6eventBus.on('dinosaurEscape', (dinosaurData) => {
7 showEmergencyAlert(dinosaurData);
8});Le module A affichera l'alarme dès que quelqu'un diffusera dinosaurEscape, bien qu'il ne sache même pas quel module le fera. Passons à l'émetteur et au deuxième écouteur.
1// Module B
2function handleDinosaurEscape(dinosaur) {
3 eventBus.emit('dinosaurEscape', {
4 id: dinosaur.id,
5 name: dinosaur.name,
6 location: dinosaur.enclosure,
7 dangerLevel: dinosaur.dangerLevel
8 });
9}
10
11// Module C
12eventBus.on('dinosaurEscape', (dinosaurData) => {
13 if (dinosaurData.dangerLevel > 8) {
14 activateEmergencyProtocols();
15 }
16});Le module B se contente de diffuser un message avec les données du dinosaure. Il ne sait pas que les modules A et C existent, et le module C décide lui-même qu'il ne réagit qu'à un niveau de danger supérieur à 8. C'est précisément le couplage faible (loose coupling) : ajouter un module D, c'est un nouveau on(), sans aucune modification du module B.
Quand utiliser un Event Bus
Dans la leçon sur les événements personnalisés, nous avons construit un bus sur EventTarget et CustomEvent, avec les données dans detail. Cette version repose sur un objet JavaScript ordinaire, elle fonctionne donc aussi en dehors du navigateur. Node.js possède d'ailleurs une classe intégrée EventEmitter, avec les mêmes méthodes on, off, once et emit.
Mon conseil : utilise le bus pour les notifications qui intéressent de nombreux modules indépendants, mais ne transforme pas chaque appel de fonction en message. Avec trop d'événements, il devient difficile de répondre à la question : qui a envoyé quoi, et pourquoi ? Garde les noms des événements dans des constantes, car une faute de frappe dans 'dinosaurEscape' ne signalera aucune erreur, elle transformera simplement l'alarme en silence radio. Dans le bac à sable ci-dessous, tu verras un bus qui relie plusieurs modules du parc. Dans la prochaine leçon, tu apprendras à tester les gestionnaires d'événements.
Retiens bien : l'Event Bus est le central radio du parc, où l'émetteur n'a pas besoin de connaître les auditeurs, et les auditeurs n'ont pas besoin de connaître l'émetteur.
Code de cette leçon : index.html
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8">
5 <title>Le pattern Event Bus</title>
6 <style>
7 body { font-family: Arial, sans-serif; background: linear-gradient(135deg, #2c3e50, #34495e); color: white; padding: 20px; }
8 .module { background: rgba(0,0,0,0.7); padding: 20px; border-radius: 10px; margin: 20px 0; border-left: 4px solid #3498db; }
9 button { padding: 10px 20px; background: #3498db; color: white; border: none; border-radius: 6px; cursor: pointer; margin: 5px; }
10 .log { background: #000; padding: 15px; border-radius: 6px; font-family: monospace; font-size: 12px; max-height: 150px; overflow-y: auto; }
11 .log-entry { padding: 3px 0; border-bottom: 1px solid #333; }
12 </style>
13</head>
14<body>
15 <h1>Architecture Event Bus</h1>
16
17 <div class="module">
18 <h3>Module A : système de sécurité</h3>
19 <button onclick="triggerSecurityAlert()">Déclencher une alerte de sécurité</button>
20 <div class="log" id="security-log"></div>
21 </div>
22
23 <div class="module">
24 <h3>Module B : gestion des visiteurs</h3>
25 <button onclick="addVisitor()">Ajouter un visiteur</button>
26 <div class="log" id="visitor-log"></div>
27 </div>
28
29 <div class="module">
30 <h3>Module C : analytics</h3>
31 <div class="log" id="analytics-log"></div>
32 </div>
33
34 <script>
35 console.log('Event Bus System initializing...');
36
37 // EVENT BUS IMPLEMENTATION
38 class EventBus {
39 constructor() {
40 this.events = {};
41 }
42
43 on(eventName, callback) {
44 if (!this.events[eventName]) {
45 this.events[eventName] = [];
46 }
47 this.events[eventName].push(callback);
48 return () => this.off(eventName, callback);
49 }
50
51 off(eventName, callback) {
52 if (this.events[eventName]) {
53 this.events[eventName] = this.events[eventName].filter(cb => cb !== callback);
54 }
55 }
56
57 emit(eventName, ...args) {
58 if (this.events[eventName]) {
59 this.events[eventName].forEach(callback => {
60 try {
61 callback(...args);
62 } catch (error) {
63 console.error(`Error in ${eventName}:`, error);
64 }
65 });
66 }
67 }
68
69 once(eventName, callback) {
70 const onceCallback = (...args) => {
71 callback(...args);
72 this.off(eventName, onceCallback);
73 };
74 this.on(eventName, onceCallback);
75 }
76 }
77
78 // Global Event Bus instance
79 const eventBus = new EventBus();
80
81 // Helper functions
82 function log(containerId, message, color = '#95a5a6') {
83 const container = document.getElementById(containerId);
84 const entry = document.createElement('div');
85 entry.className = 'log-entry';
86 entry.style.color = color;
87 entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
88 container.insertBefore(entry, container.firstChild);
89 if (container.children.length > 10) {
90 container.removeChild(container.lastChild);
91 }
92 }
93
94 // MODULE A: Security System
95 eventBus.on('security:alert', (data) => {
96 log('security-log', `ALERT: ${data.message}`, '#e74c3c');
97 log('security-log', `Location: ${data.location}`, '#e74c3c');
98 console.log('Security module received alert:', data);
99 });
100
101 eventBus.on('visitor:entered', (data) => {
102 log('security-log', `Visitor ${data.name} cleared security`, '#27ae60');
103 });
104
105 // MODULE B: Visitor Management
106 eventBus.on('visitor:entered', (data) => {
107 log('visitor-log', `New visitor: ${data.name}`, '#3498db');
108 log('visitor-log', `Ticket: ${data.ticketType}`, '#3498db');
109 });
110
111 eventBus.on('security:alert', (data) => {
112 log('visitor-log', `Evacuation notice sent`, '#f39c12');
113 });
114
115 // MODULE C: Analytics
116 let visitorCount = 0;
117 let alertCount = 0;
118
119 eventBus.on('visitor:entered', (data) => {
120 visitorCount++;
121 log('analytics-log', `Total visitors: ${visitorCount}`, '#9b59b6');
122 });
123
124 eventBus.on('security:alert', (data) => {
125 alertCount++;
126 log('analytics-log', `Total alerts: ${alertCount}`, '#9b59b6');
127 });
128
129 // Trigger functions
130 function triggerSecurityAlert() {
131 eventBus.emit('security:alert', {
132 message: 'T-Rex escaped from enclosure!',
133 location: 'Sector 7',
134 severity: 'HIGH',
135 timestamp: new Date()
136 });
137 }
138
139 function addVisitor() {
140 const names = ['John', 'Sarah', 'Mike', 'Emma'];
141 const tickets = ['Standard', 'VIP', 'Family'];
142
143 eventBus.emit('visitor:entered', {
144 name: names[Math.floor(Math.random() * names.length)],
145 ticketType: tickets[Math.floor(Math.random() * tickets.length)],
146 timestamp: new Date()
147 });
148 }
149
150 console.log('Event Bus ready');
151 console.log('3 modules connected via Event Bus');
152 log('security-log', 'Security System online', '#27ae60');
153 log('visitor-log', 'Visitor Management online', '#27ae60');
154 log('analytics-log', 'Analytics System online', '#27ae60');
155 </script>
156</body>
157</html>Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Quel est le principal avantage du pattern Event Bus dans l'architecture d'une application ?
2. Que fait la méthode once() dans une implémentation d'Event Bus ?
Tâches pratiques dans le jeu
- Éditeur de code
Le fichier index.js contient une classe EventBus avec les méthodes on(), off(), emit() et once(), comme dans la leçon sur l'Event Bus. Complète les blancs : ___BLANK1___ est la méthode de tableau qui, dans off(), garde tous les écouteurs sauf celui qu'on retire, et ___BLANK2___ est la méthode d'EventBus avec laquelle l'écouteur à usage unique de once() se désinscrit avant d'appeler le callback. emit() doit appeler les écouteurs dans l'ordre d'ajout avec les arguments transmis, off() ne retirer que l'écouteur indiqué, et un écouteur de once() ne s'exécuter qu'au premier emit().
- Tri horizontal
Place l'appel de méthode de l'Event Bus pour écouter l'événement « alert » :