W poprzednim ćwiczeniu poznaliśmy konsolę przeglądarki i jej funkcje do logowania informacji. Jednak nawet najlepsze komunikaty w konsoli mogą być niewystarczające, gdy mamy do czynienia ze skomplikowanymi błędami w systemie zarządzania Parkiem Jurajskim. Co jeśli funkcja
validateDinosaurContainment() zwraca nieprawidłowy wynik, ale nie wiemy dlaczego? Co jeśli algorytm śledzenia zachowań drapieżników zawiera subtelny błąd?W takich przypadkach potrzebujemy możliwości wstrzymania wykonania kodu w określonym miejscu i przeanalizowania stanu aplikacji. Tutaj z pomocą przychodzą breakpointy i krokowe wykonanie kodu - jedne z najpotężniejszych technik debugowania aplikacji JavaScript.
Breakpoint (punkt wstrzymania) to znacznik umieszczony w kodzie, który mówi przeglądarce, aby zatrzymała wykonanie kodu, gdy dotrze do tego miejsca. Pozwala to na dokładne zbadanie stanu aplikacji w wybranym momencie - możemy sprawdzić wartości zmiennych, obejrzeć stos wywołań i wykonać kod krok po kroku.
Istnieje kilka sposobów ustawiania breakpointów w przeglądarce:
Najprostszym sposobem jest skorzystanie z panelu Sources (Chrome) lub Debugger (Firefox):
Na przykład, możemy chcieć zatrzymać wykonanie kodu w momencie, gdy sprawdzamy status ogrodzenia w Parku Jurajskim:
1function checkFenceStatus(sectorId) {
2 const sector = getSector(sectorId); // Ustawiamy breakpoint w tej linii
3
4 if (!sector) {
5 throw new Error(`Nieznany sektor: ${sectorId}`);
6 }
7
8 const fenceStatus = sector.fence.getStatus();
9
10 if (fenceStatus !== 'active') {
11 triggerAlarm(sectorId, `Awaria ogrodzenia: ${fenceStatus}`);
12 }
13
14 return fenceStatus;
15}Po ustawieniu breakpointa, gdy funkcja
checkFenceStatus() zostanie wywołana, przeglądarka zatrzyma wykonanie kodu w tej linii, a my będziemy mogli sprawdzić wartość sectorId i inne dostępne zmienne.Czasami wygodniejsze jest umieszczenie breakpointu bezpośrednio w kodzie, używając instrukcji
debugger:1function trackDinosaur(dinosaurId, location) {
2 const dinosaur = getDinosaurById(dinosaurId);
3
4 debugger; // Kod zatrzyma się tutaj, gdy funkcja zostanie wywołana
5
6 if (!dinosaur) {
7 console.error(`Nie znaleziono dinozaura o ID: ${dinosaurId}`);
8 return false;
9 }
10
11 updateDinosaurLocation(dinosaurId, location);
12 checkContainmentStatus(dinosaurId, location);
13
14 return true;
15}Instrukcja
debugger zostanie zignorowana, jeśli narzędzia deweloperskie nie są otwarte, ale gdy są aktywne, przeglądarka zatrzyma wykonanie kodu w tym miejscu.Czasem chcemy zatrzymać wykonanie kodu tylko wtedy, gdy spełniony jest określony warunek. Na przykład, gdy śledzimy dinozaura o konkretnym ID lub gdy poziom zagrożenia przekracza określoną wartość:
dinosaurId === 'TRX-01' lub dangerLevel > 8Możemy również użyć konstrukcji warunkowej z instrukcją
debugger:1function monitorDinosaurBehavior(dinosaur) {
2 const behaviorPattern = analyzeBehavior(dinosaur);
3 const dangerLevel = assessThreatLevel(behaviorPattern);
4
5 // Zatrzymaj wykonanie tylko jeśli poziom zagrożenia jest wysoki
6 if (dangerLevel > 8) {
7 debugger; // Zatrzyma się tylko dla wysokiego poziomu zagrożenia
8
9 // Kod do obsługi niebezpiecznych zachowań
10 activateEmergencyProtocol(dinosaur.id, dangerLevel);
11 }
12
13 return {
14 dinosaurId: dinosaur.id,
15 behaviorPattern,
16 dangerLevel,
17 timestamp: new Date()
18 };
19}Gdy kod jest zatrzymany na breakpoincie, mamy kilka opcji krokowego wykonania, które pozwalają na kontrolowane przejście przez kod:
Wykonuje aktualną linię kodu i zatrzymuje się na następnej linii w tej samej funkcji. Jeśli aktualna linia zawiera wywołanie funkcji, funkcja ta jest wykonywana w całości (nie zatrzymujemy się wewnątrz niej).
1function monitorParkSystems() {
2 checkSecuritySystems(); // Step Over wykona całą tę funkcję...
3 checkDinosaurContainment(); // ...i zatrzyma się na tej linii
4 checkVisitorSafety();
5}Wykonuje aktualną linię kodu, ale jeśli linia zawiera wywołanie funkcji, debugger "wchodzi" do tej funkcji i zatrzymuje się na jej pierwszej linii.
1function monitorParkSystems() {
2 checkSecuritySystems(); // Step Into "wejdzie" do tej funkcji...
3 checkDinosaurContainment(); // ...zamiast przejść od razu tutaj
4 checkVisitorSafety();
5}
6
7function checkSecuritySystems() {
8 const fenceStatus = checkFences(); // ...i zatrzyma się tutaj
9 const cameraStatus = checkCameras();
10 const gateStatus = checkGates();
11
12 return { fenceStatus, cameraStatus, gateStatus };
13}Wykonuje pozostałą część aktualnej funkcji i zatrzymuje się po wyjściu z funkcji, w miejscu jej wywołania.
1function checkSecuritySystems() {
2 const fenceStatus = checkFences();
3 const cameraStatus = checkCameras(); // Jeśli jesteśmy tutaj...
4 const gateStatus = checkGates();
5
6 return { fenceStatus, cameraStatus, gateStatus };
7}
8
9function monitorParkSystems() {
10 checkSecuritySystems(); // ...Step Out wykona resztę funkcji i zatrzyma się tutaj
11 checkDinosaurContainment();
12 checkVisitorSafety();
13}Wznawia wykonanie kodu aż do napotkania następnego breakpointa lub zakończenia skryptu.
Gdy kod jest zatrzymany na breakpoincie, możemy szczegółowo zbadać stan aplikacji:
Jeden z najważniejszych paneli podczas debugowania - pokazuje wszystkie dostępne zmienne w aktualnym zasięgu (scope):
this w aktualnym kontekścieNa przykład, gdy debugujemy funkcję monitorującą zachowanie dinozaurów:
1function analyzeDinosaurBehavior(dinosaur, environmentalFactors) {
2 const { species, previousBehaviors, lastFeeding } = dinosaur;
3 const { weather, timeOfDay, nearbyDinosaurs } = environmentalFactors;
4
5 const timeSinceFeeding = Date.now() - lastFeeding;
6 const isHungry = timeSinceFeeding > getSpeciesFeedingThreshold(species);
7
8 debugger; // zatrzymaj się tutaj
9
10 // W tym momencie w panelu Scope zobaczymy:
11 // - Local: dinosaur, environmentalFactors, species, previousBehaviors, lastFeeding,
12 // weather, timeOfDay, nearbyDinosaurs, timeSinceFeeding, isHungry
13 // - Global: globalne obiekty i funkcje
14
15 const behaviorPrediction = predictBehavior(
16 species,
17 previousBehaviors,
18 isHungry,
19 weather,
20 timeOfDay,
21 nearbyDinosaurs
22 );
23
24 return {
25 dinosaurId: dinosaur.id,
26 prediction: behaviorPrediction,
27 riskLevel: assessRiskLevel(behaviorPrediction),
28 isHungry,
29 timestamp: new Date()
30 };
31}Panel Watch pozwala na dodanie wyrażeń, które chcemy monitorować podczas debugowania. Jest to szczególnie przydatne dla złożonych wyrażeń lub wartości, które nie są bezpośrednio widoczne w panelu Scope:
1// Możemy dodać wyrażenia obserwowane takie jak:
2dinosaur.species === 'Velociraptor'
3getDistanceFromFence(dinosaur.location)
4isInDangerZone(dinosaur.location, restrictedSectors)W trakcie zatrzymania na breakpoincie, konsola działa w kontekście aktualnej pozycji w kodzie. Możemy więc wykonywać wyrażenia, które mają dostęp do wszystkich zmiennych widocznych w bieżącym zasięgu:
1// W konsoli możemy wpisać:
2dinosaur.species
3timeSinceFeeding / (1000 * 60 * 60) // Konwersja milisekund na godziny
4nearbyDinosaurs.filter(d => d.species === 'Velociraptor')Jest to potężny sposób na interaktywne badanie stanu aplikacji i wykonywanie eksperymentów bez modyfikowania kodu.
Systemy w Parku Jurajskim często wykorzystują asynchroniczne operacje - pobieranie danych z czujników, komunikacja z systemami bezpieczeństwa, etc. Debugowanie takiego kodu wymaga specjalnych technik:
1async function monitorsEnvironmentalSensors() {
2 try {
3 console.log("Rozpoczęcie monitorowania czujników...");
4
5 // Asynchroniczne pobranie danych z czujników
6 const temperatureData = await fetchSensorData('temperature');
7 debugger; // Breakpoint zatrzyma się po otrzymaniu odpowiedzi
8
9 const humidityData = await fetchSensorData('humidity');
10 const windData = await fetchSensorData('wind');
11
12 // Analiza danych środowiskowych
13 const environmentalRisk = analyzeEnvironmentalRisk(
14 temperatureData,
15 humidityData,
16 windData
17 );
18
19 if (environmentalRisk.level === 'high') {
20 notifyParkManagement(
21 `Wykryto wysokie ryzyko środowiskowe: ${environmentalRisk.reason}`
22 );
23 }
24
25 return environmentalRisk;
26 } catch (error) {
27 console.error("Błąd monitorowania czujników:", error);
28 triggerBackupSystems();
29 return { level: 'unknown', reason: 'error' };
30 }
31}W narzędziach deweloperskich nowoczesnych przeglądarek, breakpointy działają poprawnie z asynchronicznymi funkcjami - wykonanie zatrzyma się w punkcie
debugger, nawet po operacjach await.W złożonym systemie jakim jest Park Jurajski, błędy mogą pojawiać się w nieoczekiwanych miejscach. Zamiast ręcznie ustawiać breakpointy, możemy skonfigurować debugger, aby zatrzymywał się, gdy wystąpi wyjątek:
Teraz przeglądarka zatrzyma wykonanie kodu, gdy wystąpi jakikolwiek nieobsłużony wyjątek, co znacznie ułatwia identyfikację źródła problemów.
W produkcyjnych aplikacjach kod JavaScript jest często minifikowany i bundlowany, co utrudnia debugowanie. Mapowanie źródeł pozwala przeglądarkom pokazywać i debugować oryginalny kod źródłowy, nawet gdy wykonywany jest kod zminifikowany:
1// webpack.config.js
2module.exports = {
3 //
4 devtool: 'source-map', // Generuje mapy źródeł dla wszystkich plików
5 //
6};Dla Parku Jurajskiego, gdzie bezpieczeństwo jest priorytetem, to kluczowa funkcja umożliwiająca szybkie debugowanie produkcyjnych problemów.
Czasami, zwłaszcza w złożonych systemach, warto łączyć breakpointy z logowaniem, aby śledzić wykonanie kodu:
1function monitorDinosaurMovement(dinosaurId) {
2 const dinosaur = getDinosaurById(dinosaurId);
3 console.log(`[Checkpoint 1] Rozpoczęcie monitorowania ${dinosaur.name}`);
4
5 if (!dinosaur.tracker.isActive) {
6 console.warn(`[Checkpoint 2A] Nieaktywny tracker dla ${dinosaur.name}`);
7 activateTracker(dinosaurId);
8 debugger; // Zatrzymaj się tylko, gdy tracker był nieaktywny
9 } else {
10 console.log(`[Checkpoint 2B] Tracker aktywny dla ${dinosaur.name}`);
11 }
12
13 const location = getLastKnownLocation(dinosaurId);
14 console.log(`[Checkpoint 3] Ostatnia lokalizacja: ${JSON.stringify(location)}`);
15
16 // Różne ścieżki bazujące na lokalizacji
17 if (isInRestrictedZone(location)) {
18 console.error(`[Checkpoint 4A] Dinozaur w strefie ograniczonej!`);
19 debugger; // Zawsze zatrzymaj się w przypadku naruszenia strefy
20 triggerContainmentProtocol(dinosaurId, location);
21 } else if (isApproachingFence(location)) {
22 console.warn(`[Checkpoint 4B] Dinozaur zbliża się do ogrodzenia`);
23 increaseSurveillance(dinosaurId);
24 } else {
25 console.log(`[Checkpoint 4C] Dinozaur w normalnej strefie`);
26 }
27
28 return {
29 dinosaurId,
30 location,
31 status: getCurrentStatus(dinosaurId)
32 };
33}Taki wzorzec "checkpointów" pomaga śledzić przepływ wykonania i szybko identyfikować, która ścieżka kodu została wykonana, nawet gdy nie korzystamy aktywnie z debuggera.
Przeanalizujmy praktyczny przykład debugowania problemu w systemie monitorowania dinozaurów:
1class DinosaurMonitoringSystem {
2 constructor() {
3 this.activeMonitors = new Map();
4 this.alertLevel = 'normal';
5 this.lastScanTime = null;
6 }
7
8 startMonitoring(dinosaurId) {
9 if (this.activeMonitors.has(dinosaurId)) {
10 console.log(`Monitoring dla dinozaura ${dinosaurId} już aktywny`);
11 return false;
12 }
13
14 const dinosaur = getDinosaurById(dinosaurId);
15 if (!dinosaur) {
16 console.error(`Nie znaleziono dinozaura o ID: ${dinosaurId}`);
17 return false;
18 }
19
20 // Tworzymy nowy monitor i dodajemy do aktywnych
21 const monitor = {
22 dinosaurId,
23 species: dinosaur.species,
24 startTime: Date.now(),
25 lastUpdate: Date.now(),
26 status: 'active',
27 readings: [],
28 checkInterval: getSpeciesCheckInterval(dinosaur.species)
29 };
30
31 this.activeMonitors.set(dinosaurId, monitor);
32
33 // Uruchamiamy interwał do regularnego sprawdzania
34 const intervalId = setInterval(() => {
35 this.checkDinosaurStatus(dinosaurId);
36 }, monitor.checkInterval);
37
38 monitor.intervalId = intervalId;
39
40 console.log(`Rozpoczęto monitoring dinozaura ${dinosaurId} (${dinosaur.species})`);
41 return true;
42 }
43
44 checkDinosaurStatus(dinosaurId) {
45 const monitor = this.activeMonitors.get(dinosaurId);
46
47 if (!monitor) {
48 console.error(`Brak aktywnego monitoringu dla dinozaura ${dinosaurId}`);
49 return null;
50 }
51
52 try {
53 // Pobieranie aktualnych danych
54 const dinosaur = getDinosaurById(dinosaurId);
55 const location = getLastKnownLocation(dinosaurId);
56 const vitalSigns = getVitalSigns(dinosaurId);
57 const behavior = analyzeBehavior(dinosaurId, location);
58
59 // BŁĄD: Nieprawidłowe obliczenie statusu zdrowia
60 const healthStatus = calculateHealthStatus(vitalSigns);
61
62 // BŁĄD: Nie sprawdzamy czy location istnieje!
63 const inContainment = isInContainmentZone(location.coordinates);
64
65 const reading = {
66 timestamp: Date.now(),
67 location,
68 vitalSigns,
69 behavior,
70 healthStatus,
71 inContainment
72 };
73
74 // Aktualizujemy monitor
75 monitor.readings.push(reading);
76 monitor.lastUpdate = reading.timestamp;
77 monitor.locationStatus = inContainment ? 'contained' : 'breach';
78 monitor.healthStatus = healthStatus;
79
80 // Sprawdzamy poziom zagrożenia
81 this.assessThreatLevel();
82
83 return reading;
84 } catch (error) {
85 console.error(`Błąd sprawdzania statusu dinozaura ${dinosaurId}:`, error);
86 monitor.lastError = {
87 timestamp: Date.now(),
88 message: error.message
89 };
90
91 return null;
92 }
93 }
94
95 stopMonitoring(dinosaurId) {
96 const monitor = this.activeMonitors.get(dinosaurId);
97
98 if (!monitor) {
99 console.warn(`Brak aktywnego monitoringu dla dinozaura ${dinosaurId}`);
100 return false;
101 }
102
103 // Zatrzymujemy interwał
104 clearInterval(monitor.intervalId);
105
106 // Usuwamy z aktywnych monitorów
107 this.activeMonitors.delete(dinosaurId);
108
109 console.log(`Zatrzymano monitoring dinozaura ${dinosaurId}`);
110 return true;
111 }
112
113 assessThreatLevel() {
114 // Logika oceny poziomu zagrożenia na podstawie wszystkich monitorów
115 let highestThreat = 'normal';
116
117 for (const [id, monitor] of this.activeMonitors.entries()) {
118 if (!monitor.locationStatus || monitor.locationStatus === 'breach') {
119 highestThreat = 'critical';
120 break;
121 }
122
123 if (monitor.healthStatus === 'agitated' && highestThreat !== 'critical') {
124 highestThreat = 'elevated';
125 }
126 }
127
128 // Aktualizujemy poziom alertu
129 if (highestThreat !== this.alertLevel) {
130 console.log(`Zmiana poziomu alertu: ${this.alertLevel} -> ${highestThreat}`);
131 this.alertLevel = highestThreat;
132
133 // Powiadomienie o zmianie poziomu alertu
134 notifyAlertLevelChange(this.alertLevel);
135 }
136
137 return this.alertLevel;
138 }
139}Podczas testowania systemu monitorowania w Parku Jurajskim, odkrywamy, że system nieoczekiwanie zgłasza poziom alertu 'critical' dla niektórych dinozaurów, mimo że są one bezpiecznie w swoich wybiegach.
Ustawiamy breakpoint w metodzie
checkDinosaurStatus, aby zbadać, co się dzieje:1checkDinosaurStatus(dinosaurId) {
2 const monitor = this.activeMonitors.get(dinosaurId);
3
4 if (!monitor) {
5 console.error(`Brak aktywnego monitoringu dla dinozaura ${dinosaurId}`);
6 return null;
7 }
8
9 debugger; // Ustawiamy breakpoint tutaj, aby zbadać wszystkie zmienne
10
11 try {
12 // ...reszta kodu
13 }
14}1// BŁĄD: Nie sprawdzamy czy location istnieje!
2const inContainment = isInContainmentZone(location.coordinates);location w panelu Scope i widzimy, że jest nullgetLastKnownLocation czasem zwraca null, gdy nie może znaleźć lokalizacji, a kod nie sprawdza tego przypadku przed próbą dostępu do location.coordinatesModyfikujemy kod, aby obsługiwał przypadek, gdy
location jest null:1// Bezpieczniejszy kod z weryfikacją
2const location = getLastKnownLocation(dinosaurId);
3const inContainment = location && location.coordinates
4 ? isInContainmentZone(location.coordinates)
5 : false; // Jeśli nie znamy lokalizacji, zakładamy najgorszy scenariuszPo naprawieniu błędu, usuwamy breakpoint i testujemy system ponownie. Tym razem działa poprawnie, nie zgłaszając fałszywych alarmów.
Breakpointy i krokowe wykonanie kodu to nieocenione narzędzia w arsenale każdego programisty pracującego przy systemach Parku Jurajskiego. Pozwalają na:
Pamiętaj, że w Parku Jurajskim, umiejętność szybkiego znalezienia i naprawienia błędów może być kwestią życia lub śmierci - zarówno kodu, jak i (w prawdziwym parku) odwiedzających!