Kurs JavaScript i TypeScript · Moduł 8: Zdarzenia i DOM
Event Bus i architektura aplikacji
Implementacja systemu komunikacji między komponentami za pomocą event bus.
Wyobraź sobie moduł wykrywający ucieczki dinozaurów. Gdy raptor sforsuje ogrodzenie, trzeba włączyć syrenę, zaznaczyć strefę na mapie, zablokować bramy i wysłać wiadomość do ochrony. Jeśli moduł ucieczek wywołuje każdą z tych funkcji bezpośrednio, każdy nowy system oznacza grzebanie w jego kodzie, a po roku nikt nie wie, co od czego zależy. Event Bus działa jak centrala radiowa parku: moduły nadają komunikaty na wspólnej szynie, a zainteresowani sami ich słuchają. Nikt nie musi znać adresu nikogo.
Implementacja Event Bus
Klasa przechowuje listenery w obiekcie events. Kluczem jest nazwa wydarzenia, a wartością tablica funkcji, które na nie czekają.
class EventBus {
constructor() {
this.events = {};
}
// Rejestracja nasłuchiwacza
on(eventName, callback) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
this.events[eventName].push(callback);
// Zwróć funkcję do usunięcia listenera
return () => this.off(eventName, callback);
}Metoda on() tworzy tablicę przy pierwszej rejestracji, dopisuje do niej funkcję i zwraca gotową funkcję do wyrejestrowania. Dzięki temu moduł nie musi przechowywać referencji do callbacka tylko po to, żeby go później usunąć. Zapis eventBus.on('alert', callback) czyta się jak zdanie: nasłuchuj na alert i wtedy wywołaj callback. Dwie kolejne metody zdejmują listenery.
// Usunięcie nasłuchiwacza
off(eventName, callback) {
if (this.events[eventName]) {
this.events[eventName] = this.events[eventName].filter(cb => cb !== callback);
}
}
// Jednorazowy nasłuchiwacz
once(eventName, callback) {
const onceCallback = (...args) => {
callback(...args);
this.off(eventName, onceCallback);
};
this.on(eventName, onceCallback);
}Metoda off() nie modyfikuje starej tablicy, tylko tworzy nową przez filter(), więc usunięcie listenera w trakcie emisji nie psuje trwającej pętli. Metoda once() owija callback w onceCallback, który najpierw wywołuje oryginał, a potem sam się wyrejestrowuje. Listener zadziała więc dokładnie raz i zniknie automatycznie. Ta wersja ma jedno ograniczenie: zapisaliśmy onceCallback, a nie oryginalny callback, więc wywołanie off() z oryginałem go nie usunie. Czas na nadawanie komunikatów.
// Emisja wydarzenia
emit(eventName, ...args) {
if (this.events[eventName]) {
this.events[eventName].forEach(callback => {
try {
callback(...args);
} catch (error) {
console.error(`Error in event listener for ${eventName}:`, error);
}
});
}
}Metoda emit() przekazuje wszystkie argumenty każdemu słuchaczowi w kolejności rejestracji. Blok try...catch wokół każdego wywołania to ważna decyzja architektoniczna: błąd w jednym module trafia do konsoli, ale nie zatrzymuje powiadomień dla pozostałych. Syrena zawyje, nawet jeśli moduł mapy akurat ma usterkę. Na koniec sprzątanie hurtowe.
// Usunięcie wszystkich listenerów dla wydarzenia
removeAllListeners(eventName) {
if (eventName) {
delete this.events[eventName];
} else {
this.events = {};
}
}
}Metoda removeAllListeners() z nazwą usuwa jeden kanał, a bez argumentu czyści całą szynę. Przydaje się przy zamykaniu aplikacji i w testach, gdy każdy test ma startować z czystą centralą.
Użycie w modułach aplikacji
W aplikacji tworzymy jedną wspólną instancję, którą importują wszystkie moduły. Moduł A zapisuje się na wiadomość o ucieczce.
// Globalna instancja event bus
const eventBus = new EventBus();
// Użycie w różnych modułach aplikacji
// Module A
eventBus.on('dinosaurEscape', (dinosaurData) => {
showEmergencyAlert(dinosaurData);
});Moduł A pokaże alarm, gdy tylko ktoś nada dinosaurEscape, choć nie wie nawet, który moduł to zrobi. Teraz nadawca i drugi słuchacz.
// Module B
function handleDinosaurEscape(dinosaur) {
eventBus.emit('dinosaurEscape', {
id: dinosaur.id,
name: dinosaur.name,
location: dinosaur.enclosure,
dangerLevel: dinosaur.dangerLevel
});
}
// Module C
eventBus.on('dinosaurEscape', (dinosaurData) => {
if (dinosaurData.dangerLevel > 8) {
activateEmergencyProtocols();
}
});Moduł B tylko nadaje komunikat z danymi dinozaura. Nie wie, że istnieją moduły A i C, a moduł C sam decyduje, że reaguje wyłącznie na poziom zagrożenia powyżej 8. To właśnie luźne powiązanie (loose coupling): dodanie modułu D to jedno nowe on(), bez żadnej zmiany w module B.
Kiedy sięgać po Event Bus
W lekcji o własnych wydarzeniach zbudowaliśmy szynę na EventTarget i CustomEvent, z danymi w detail. Ta wersja opiera się na zwykłym obiekcie JavaScript, więc działa także poza przeglądarką. Node.js ma zresztą wbudowaną klasę EventEmitter z tymi samymi metodami on, off, once i emit.
Moja rada: używaj szyny do powiadomień, które interesują wiele niezależnych modułów, ale nie zamieniaj w komunikaty każdego wywołania funkcji. Przy nadmiarze wydarzeń trudno odpowiedzieć na pytanie, kto co wysłał i dlaczego. Nazwy wydarzeń trzymaj w stałych, bo literówka w 'dinosaurEscape' nie zgłosi błędu, tylko zamieni alarm w ciszę w eterze. W piaskownicy poniżej zobaczysz szynę łączącą kilka modułów parku. W kolejnej lekcji nauczysz się testować handlery wydarzeń.
Pamiętaj: Event Bus to centrala radiowa parku, w której nadawca nie musi znać słuchaczy, a słuchacze nie muszą znać nadawcy.
Kod do tej lekcji: index.html
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<title>Event Bus Pattern</title>
<style>
body { font-family: Arial, sans-serif; background: linear-gradient(135deg, #2c3e50, #34495e); color: white; padding: 20px; }
.module { background: rgba(0,0,0,0.7); padding: 20px; border-radius: 10px; margin: 20px 0; border-left: 4px solid #3498db; }
button { padding: 10px 20px; background: #3498db; color: white; border: none; border-radius: 6px; cursor: pointer; margin: 5px; }
.log { background: #000; padding: 15px; border-radius: 6px; font-family: monospace; font-size: 12px; max-height: 150px; overflow-y: auto; }
.log-entry { padding: 3px 0; border-bottom: 1px solid #333; }
</style>
</head>
<body>
<h1>Event Bus Architecture</h1>
<div class="module">
<h3>Module A: Security System</h3>
<button onclick="triggerSecurityAlert()">Trigger Security Alert</button>
<div class="log" id="security-log"></div>
</div>
<div class="module">
<h3>Module B: Visitor Management</h3>
<button onclick="addVisitor()">Add Visitor</button>
<div class="log" id="visitor-log"></div>
</div>
<div class="module">
<h3>Module C: Analytics</h3>
<div class="log" id="analytics-log"></div>
</div>
<script>
console.log('Event Bus System initializing...');
// EVENT BUS IMPLEMENTATION
class EventBus {
constructor() {
this.events = {};
}
on(eventName, callback) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
this.events[eventName].push(callback);
return () => this.off(eventName, callback);
}
off(eventName, callback) {
if (this.events[eventName]) {
this.events[eventName] = this.events[eventName].filter(cb => cb !== callback);
}
}
emit(eventName, ...args) {
if (this.events[eventName]) {
this.events[eventName].forEach(callback => {
try {
callback(...args);
} catch (error) {
console.error(`Error in ${eventName}:`, error);
}
});
}
}
once(eventName, callback) {
const onceCallback = (...args) => {
callback(...args);
this.off(eventName, onceCallback);
};
this.on(eventName, onceCallback);
}
}
// Global Event Bus instance
const eventBus = new EventBus();
// Helper functions
function log(containerId, message, color = '#95a5a6') {
const container = document.getElementById(containerId);
const entry = document.createElement('div');
entry.className = 'log-entry';
entry.style.color = color;
entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
container.insertBefore(entry, container.firstChild);
if (container.children.length > 10) {
container.removeChild(container.lastChild);
}
}
// MODULE A: Security System
eventBus.on('security:alert', (data) => {
log('security-log', `ALERT: ${data.message}`, '#e74c3c');
log('security-log', `Location: ${data.location}`, '#e74c3c');
console.log('Security module received alert:', data);
});
eventBus.on('visitor:entered', (data) => {
log('security-log', `Visitor ${data.name} cleared security`, '#27ae60');
});
// MODULE B: Visitor Management
eventBus.on('visitor:entered', (data) => {
log('visitor-log', `New visitor: ${data.name}`, '#3498db');
log('visitor-log', `Ticket: ${data.ticketType}`, '#3498db');
});
eventBus.on('security:alert', (data) => {
log('visitor-log', `Evacuation notice sent`, '#f39c12');
});
// MODULE C: Analytics
let visitorCount = 0;
let alertCount = 0;
eventBus.on('visitor:entered', (data) => {
visitorCount++;
log('analytics-log', `Total visitors: ${visitorCount}`, '#9b59b6');
});
eventBus.on('security:alert', (data) => {
alertCount++;
log('analytics-log', `Total alerts: ${alertCount}`, '#9b59b6');
});
// Trigger functions
function triggerSecurityAlert() {
eventBus.emit('security:alert', {
message: 'T-Rex escaped from enclosure!',
location: 'Sector 7',
severity: 'HIGH',
timestamp: new Date()
});
}
function addVisitor() {
const names = ['John', 'Sarah', 'Mike', 'Emma'];
const tickets = ['Standard', 'VIP', 'Family'];
eventBus.emit('visitor:entered', {
name: names[Math.floor(Math.random() * names.length)],
ticketType: tickets[Math.floor(Math.random() * tickets.length)],
timestamp: new Date()
});
}
console.log('Event Bus ready');
console.log('3 modules connected via Event Bus');
log('security-log', 'Security System online', '#27ae60');
log('visitor-log', 'Visitor Management online', '#27ae60');
log('analytics-log', 'Analytics System online', '#27ae60');
</script>
</body>
</html>Hasła z dokumentacji w tej lekcji
Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Jaka jest główna zaleta wzorca Event Bus w architekturze aplikacji?
2. Co robi metoda once() w implementacji Event Bus?
Zadania praktyczne w grze
- Edytor kodu
W pliku index.js jest klasa EventBus z metodami on(), off(), emit() i once(), jak w lekcji o Event Bus. Uzupełnij luki: ___BLANK1___ to metoda tablicy, która w off() zostawia wszystkie listenery poza usuwanym, a ___BLANK2___ to metoda EventBus, za pomocą której listener jednorazowy z once() wypisuje się przed wywołaniem callbacku. emit() ma wywołać listenery w kolejności dodania z przekazanymi argumentami, off() usunąć tylko wskazany listener, a listener z once() zadziałać tylko przy pierwszym emit().
- Układanie w poziomie
Ułóż wywołanie metody Event Bus do nasłuchiwania na wydarzenie 'alert':