Cours JavaScript et TypeScript · Module 8: Événements et DOM

Event Bus et architecture applicative

5 min de lecture

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. 1. Quel est le principal avantage du pattern Event Bus dans l'architecture d'une application ?

  2. 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 » :

Articles utiles