Debugging-Techniken im Browser
(alan.norbauer.com)- Wenn man die Browser-Entwicklertools mindestens auf mittlerem Niveau beherrscht, lassen sich bedingte Breakpoints nicht nur als einfache Stopp-Bedingung, sondern auch als Werkzeug für Logging, Tracing, Statusänderungen und Zeitmessung erweitern
- Wenn man Console-APIs wie
console.log,console.count,console.traceundconsole.timein Breakpoints oder in den Watch Pane einfügt, lassen sich Ablauf und Zustand protokollieren, ohne die Ausführung anzuhalten - Durch die Kombination aus Anzahl der Funktionsargumente, Zeit, berechneten CSS-Werten, Ausführungshäufigkeit, zufälligem Sampling und globalen Booleans lässt sich der Zeitpunkt des Anhaltens deutlich präziser steuern
- Chromes
monitor,debug,copy,$0,getEventListenersundmonitorEventshelfen dabei, Funktionsaufrufe, DOM, Events und Clipboard-Aufgaben schnell über die Konsole zu handhaben - Auf Oberflächen, deren DOM sich fortlaufend durch JS oder Nutzerereignisse verändert, sind JS-Pause,
setTimeout(debugger), DOM-Snapshots und Fokusüberwachung besonders nützlich für das HTML/CSS-Debugging
Bedingte Breakpoints zu Debugging-Werkzeugen erweitern
-
In bedingte Breakpoints lassen sich nicht nur Wahr/Falsch-Bedingungen einfügen, sondern auch JavaScript-Ausdrücke mit Seiteneffekten
-
So kann man gewöhnliche Breakpoints wie Werkzeuge für Logging, Tracing, Zustandsänderungen oder Zeitmessung einsetzen
-
Logpoints und Tracepoints
- Wenn man in einem bedingten Breakpoint
console.logaufruft, verhält er sich wie ein Logpoint, der Werte in der Konsole ausgibt, ohne die Ausführung anzuhalten - Mit
console.countlässt sich außerdem mitzählen, wie oft die betreffende Zeile ausgeführt wurde - Stand des Updates von Mai 2020 unterstützen die wichtigsten Browser Logpoints/Tracepoints direkt
- Wenn man in einem bedingten Breakpoint
-
Console-APIs im Watch Pane verwenden
- Auch in den Watch Pane lassen sich Ausdrücke wie
console.logeinfügen - Jedes Mal, wenn die Anwendung im Debugger anhält, kann man mit
console.table(localStorage)einen localStorage-Snapshot aufnehmen - Verwendet man DOM-Change-Breakpoints zusammen mit Watch-Ausdrücken, lassen sich nach Änderungen an einem DOM-Subtree Snapshots in einem Array speichern
(window.doms = window.doms || []).push(document.documentElement.outerHTML)- DOM-Change-Breakpoints lassen sich nicht so konfigurieren, dass sie ohne Anhalten laufen
- Auch in den Watch Pane lassen sich Ausdrücke wie
-
Call Stack nachverfolgen
- Wenn es eine Funktion zum Anzeigen und eine zum Verbergen eines Ladespinners gibt und man einen Fehler sucht, bei dem nur der Aufruf zum Anzeigen übrig bleibt, kann man
console.tracein einen bedingten Breakpoint einsetzen - Klickt man im letzten Stack Trace der Anzeigefunktion auf den Aufrufer, springt man direkt zur betreffenden Aufrufstelle
- Wenn es eine Funktion zum Anzeigen und eine zum Verbergen eines Ladespinners gibt und man einen Fehler sucht, bei dem nur der Aufruf zum Anzeigen übrig bleibt, kann man
-
Programmverhalten zur Laufzeit ändern
- Überschreibt man Funktionsargumente im Ausdruck eines bedingten Breakpoints, kann man das Programmverhalten direkt im Browser spontan ändern
- So lässt sich zum Beispiel der
id-Wert der FunktiongetPersonverändern - Ein Ausdruck wie
id=1, der zu wahr ausgewertet wird, hält den Debugger an; wenn das nicht passieren soll, kann man, falseanhängen
-
Schnelle Performance-Messung
- Da sich die Auswertungszeit bedingter Breakpoints einmischt, eignet sich das nicht für präzises Performance-Profiling
- Wenn man schnell messen will, kann man in die Bedingung am Startpunkt
console.time('label')und am Endpunktconsole.timeEnd('label')einfügen, um die Laufzeit in der Konsole auszugeben
Stopp-Bedingungen präziser machen
-
Anzahl der Funktionsargumente nutzen
- Wenn nur dann angehalten werden soll, wenn die aktuelle Funktion mit 3 Argumenten aufgerufen wird, kann man folgende Bedingung verwenden
arguments.callee.length === 3- Das ist nützlich bei überladenen Funktionen mit optionalen Argumenten
- Wenn nur dann angehalten werden soll, wenn sich die definierte Anzahl von Argumenten von der tatsächlich übergebenen Anzahl unterscheidet, kann man folgende Bedingung verwenden
(arguments.callee.length) != arguments.length- Das hilft beim Finden von Bugs an den Funktionsaufrufstellen
-
Zeitbasierte Bedingungen
- Wenn in den ersten 5 Sekunden nach dem Laden der Seite nicht angehalten werden soll, kann man folgende Bedingung verwenden
performance.now() > 5000- Das eignet sich, wenn Breakpoints erst nach dem initialen Laden der Seite relevant sind
- Wenn ein Breakpoint nach dem Setzen für die nächsten 5 Sekunden nicht anhalten und erst danach aktiv werden soll, kann man die Referenzzeit in einer globalen Variable speichern
window.baseline = window.baseline || Date.now(), (Date.now() - window.baseline) > 5000- In der Konsole kann man den Zähler mit
window.baseline = Date.now()neu starten
-
CSS-Zustandsbasierte Bedingungen
- Man kann bei berechneten CSS-Werten als Bedingung anhalten
- Zum Beispiel kann nur dann angehalten werden, wenn die Hintergrundfarbe von
document.bodyrot ist
window.getComputedStyle(document.body).backgroundColor === "rgb(255,0,0)" -
Ausführungshäufigkeit und Sampling
- Erhöht man bei jeder Ausführung der Zeile einen Zähler, kann man nur bei jeder zweiten Ausführung anhalten
window.counter = (window.counter || 0) + 1, window.counter % 2 === 0- Für zufälliges Sampling, bei dem nur in 1 von 10 Fällen angehalten wird, kann man folgende Bedingung verwenden
Math.random() < 0.1 -
An bestimmten Stellen niemals anhalten
- Wenn man in Chrome per Rechtsklick auf den Gutter „Never Pause Here“ auswählt, wird ein bedingter Breakpoint erzeugt, dessen Bedingung immer
falseist, sodass an dieser Zeile nie angehalten wird - Das lässt sich nutzen, um bestimmte Zeilen bei XHR-Breakpoints auszuschließen oder bestimmte Ausnahmen zu ignorieren
- Wenn man in Chrome per Rechtsklick auf den Gutter „Never Pause Here“ auswählt, wird ein bedingter Breakpoint erzeugt, dessen Bedingung immer
-
Instanz-IDs automatisch vergeben
- Fügt man in einen Klassenkonstruktor einen bedingten Breakpoint ein, lassen sich alle Instanzen in einem Array speichern
(window.instances = window.instances || []).push(this)- Die eindeutige ID einer bestimmten Instanz lässt sich über den Array-Index bestimmen
window.instances.indexOf(instance) -
Globales Umschalten für Breakpoints
- Ein oder mehrere bedingte Breakpoints lassen sich über ein globales Boolean steuern
- Man kann es direkt in der Konsole ändern, über einen anderen Breakpoint umschalten oder per Timer setzen
window.enableBreakpoints = true setTimeout(() => (window.enableBreakpoints = true), 5000)
Funktions- und Klassenaufrufe nachverfolgen
-
Aufrufe von Klassenmethoden überwachen
- Mit der Chrome-Kommandozeilenmethode
monitorlassen sich Aufrufe von Klassenmethoden leicht verfolgen - Wenn man Methodenaufrufe aller Instanzen der Klasse
Dogsehen will, kann manmonitorauf alle Properties des Prototyps anwenden
var p = Dog.prototype; Object.getOwnPropertyNames(p).forEach((k) => monitor(p[k]));- In der Konsole erscheinen dann Aufrufinformationen wie
function bark called with arguments: 2 - Wenn man nicht nur loggen, sondern bei jedem Aufruf anhalten will, kann man statt
monitorauchdebugverwenden
- Mit der Chrome-Kommandozeilenmethode
-
Klassen-Tracking an einer bestimmten Instanz
- Wenn man den Klassennamen nicht kennt, aber eine Instanz hat, kann man auf dieselbe Weise über den Prototype des Konstruktors überwachen
var p = instance.constructor.prototype; Object.getOwnPropertyNames(p).forEach((k) => monitor(p[k]));- Das ist nützlich, wenn man eine Funktion schreiben will, die mit beliebigen Instanzen funktioniert, statt auf eine bestimmte Klasse festgelegt zu sein
-
Vor einem Funktionsaufruf aus der Konsole debuggen
- Wenn man eine zu debuggende Funktion aus der Konsole aufrufen will, kann man vorher
debuggerausführen und dann mit dem nächsten Funktionsaufruf per Step into in die Implementierung wechseln
debugger; fn(1);- Das ist praktisch, wenn das Auffinden der Funktionsdefinition mühsam wäre oder
fndynamisch gebunden ist und man die Position im Quellcode nicht kennt - In Chrome kann man auch
debug(fn)aufrufen, damit bei jedem Aufruf vonfninnerhalb der Funktion angehalten wird
- Wenn man eine zu debuggende Funktion aus der Konsole aufrufen will, kann man vorher
Änderungen an URLs und Property-Lesezugriffe debuggen
-
Vor SPA-Routing anhalten
- Wenn man kurz vor einer URL-Änderung in einer Single-Page-Application anhalten will, kann man Funktionen mit
debuggeranhistory.pushState,history.replaceState,window.onhashchangeundwindow.onpopstatehängen
const dbg = () => { debugger; }; history.pushState = dbg; history.replaceState = dbg; window.onhashchange = dbg; window.onpopstate = dbg;- Diese Version von
dbgenthält noch keine Implementierung, die anhält, ohne die Navigation zu zerstören - Wenn
window.location.replaceoderwindow.location.assigndirekt aufgerufen werden, wird die Seite unmittelbar nach der Zuweisung entladen, sodass diese Methode dort nicht greift - In Chrome kann man auf die zugehörigen Methoden
debugsetzen, um die Quelle der Weiterleitung und den Zustand zu diesem Zeitpunkt zu sehen
debug(window.location.replace); debug(window.location.assign); - Wenn man kurz vor einer URL-Änderung in einer Single-Page-Application anhalten will, kann man Funktionen mit
-
Stellen finden, an denen Properties gelesen werden
- Wenn man wissen will, wann eine bestimmte Property eines Objekts gelesen wird, kann man
debuggerin einen Getter einfügen
{ get configOption() { debugger; return true; } }- Das ist nützlich, um zu prüfen, welcher Code eine Konfigurationsoption wie verwendet
- Man kann das im Originalquellcode ändern oder per bedingtem Breakpoint anwenden
- Wenn man wissen will, wann eine bestimmte Property eines Objekts gelesen wird, kann man
Daten aus der Konsole kopieren
- Mit der Console-API
copy()lassen sich Informationen aus dem Browser ohne abgeschnittene Strings in die Zwischenablage kopieren - Das ist nützlich für den aktuellen DOM-Snapshot, Ressourcen-Metadaten wie bei Bildern, große JSON-Blobs oder einen localStorage-Dump
copy(document.documentElement.outerHTML)
copy(performance.getEntriesByType("resource"))
copy(JSON.parse(blob))
copy(localStorage)
HTML/CSS-Debugging
-
DOM prüfen, während die JS-Ausführung angehalten ist
- Im DOM Inspector kann man in Chrome unter Windows mit
ctrl+\jederzeit die JS-Ausführung anhalten - Auch wenn JS das DOM verändert oder Ereignisse wie
mouseoverdas DOM ändern, lässt sich der aktuelle DOM-Snapshot stabil untersuchen
- Im DOM Inspector kann man in Chrome unter Windows mit
-
Verschwindende Elemente prüfen
- Ein bedingtes DOM-Element, das beim Bewegen der Maus verschwindet, kann man untersuchen, indem man in der Konsole einen 5-Sekunden-Timer setzt, der den Debugger startet
setTimeout(function() { debugger; }, 5000);- Wenn man die UI in diesen 5 Sekunden öffnet und die JS-Ausführung dann angehalten wird, verschwindet das Element nicht mehr, sodass man zu den Entwicklertools wechseln und es untersuchen kann
- Während die JS-Ausführung angehalten ist, sind Elementinspektion, CSS-Bearbeitung und JS-Konsolenbefehle weiterhin möglich
- Das ist besonders effektiv, wenn man DOM untersuchen will, das von Cursorposition oder Fokus abhängt
-
DOM-Snapshots aufzeichnen
- Um das aktuelle DOM zu kopieren, kann man den folgenden Befehl verwenden
copy(document.documentElement.outerHTML);- Um jede Sekunde einen DOM-String in einem Array zu speichern, kann man
setIntervalwie folgt verwenden
doms = []; setInterval(() => { const domStr = document.documentElement.outerHTML; doms.push(domStr); }, 1000);- Statt im Array zu speichern, kann man die Ausgabe auch fortlaufend in die Konsole schreiben
-
Fokussiertes Element überwachen
- Wenn man
document.activeElementregelmäßig vergleicht, kann man Fokusänderungen in der Konsole ausgeben
(function () { let last = document.activeElement; setInterval(() => { if (document.activeElement !== last) { last = document.activeElement; console.log("Focus changed to: ", last); } }, 100); })(); - Wenn man
-
Fett dargestellte Elemente finden
- Unter allen Elementen lassen sich diejenigen finden, deren berechnetes
fontWeight"bold"oder"700"ist
const isBold = (e) => { let w = window.getComputedStyle(e).fontWeight; return w === "bold" || w === "700"; }; Array.from(document.querySelectorAll("*")).filter(isBold);- Wenn man nur unter den Nachfahren des aktuell im Inspector ausgewählten Elements suchen will, kann man auf Basis von
$0suchen
Array.from($0.querySelectorAll("*")).filter(isBold); - Unter allen Elementen lassen sich diejenigen finden, deren berechnetes
-
Mit ausgewählten Elementen und Events arbeiten
$0in der Konsole ist eine automatische Referenz auf das aktuell im Element Inspector ausgewählte Element- In Chrome und Edge kann man zuvor untersuchte Elemente über
$1, davor über$2usw. ansprechen - In Chrome kann man die Event-Listener des aktuell ausgewählten Elements anzeigen
getEventListeners($0)- Um alle Events des ausgewählten Elements zu debuggen, kann man
monitorEvents($0)verwenden - Wenn man nur bestimmte Event-Gruppen sehen will, kann man ein Array übergeben
monitorEvents($0, ["control", "key"])
1 Kommentare
Meinungen auf Hacker News
In den letzten Jahrzehnten haben sich die im Browser integrierten Debugging-Tools wirklich enorm weiterentwickelt.
Aus der Sicht von jemandem, der schon lange JS nutzt, bin ich den Leuten zutiefst dankbar, die das Debuggen von Code im Browser so intuitiv gemacht haben.
Wenn man in andere Entwicklungsbereiche wie Backend oder andere Sprachen wechselt, vermisst man dieses Ökosystem an Debugging-Tools, das moderne Browser standardmäßig mitbringen.
Ich habe über Jahre viel in Python, Java und C++ debuggt, hatte aber nie das Gefühl, dass es an Tools mangelt.
Allerdings habe ich durchaus viele Leute gesehen, die die in diesem Bereich verfügbaren Optionen nicht kannten.
Statt dieser Hölle nehme ich lieber die Konsole.
setTimeout(function() { debugger; }, 5000);ist clever.Letztlich ist die einzige Möglichkeit, wie der Chrome-Debugger den rekursiven Schildkröten-Stack besiegt, in dem er sich selbst debuggt, die
debugger-Anweisung.Auch
sam.pldes berüchtigten Myspace-Sammy-Wurms nutzte Debugging-Fallen, um Besucher davon abzuhalten, die obfuskierte HTML-Homepage auseinanderzunehmen.Sie lässt die Main Loop laufen, fügt überall nervige
debugger;-Anweisungen ein und führt sie mit 30 FPS / in 32-ms-Intervallen aus, wodurch DevTools unbrauchbar wird.Denn es gibt keine Möglichkeit,
debugger-Anweisungen zu „ignorieren“.cmd-\für Skriptausführung pausieren verwenden.Die Seite hält bei der gerade ausgeführten JS-Zeile an, und man kann das problematische Element wie gewohnt untersuchen.
debugger-Anweisungen ignorieren.Die meisten legitimen Anwendungsfälle für
debugger-Anweisungen lassen sich, wenn man am eigenen Code arbeitet, einfach durch normale Breakpoints ersetzen.Auffällig ist, dass
queryObjectsfehlt.Das ist eine ziemlich verrückte API, die eine Liste aller Objekte zurückgibt, die mit einem bestimmten Konstruktor erzeugt wurden.
Zum Beispiel kann man mit
queryObjects(Function)eine Liste aller Funktionen im Heap bekommen.Sie gibt sogar „private“ Funktionen zurück, die sich in irgendeinem Modul befinden.
Es ist eine wirklich verrückte Funktion und vermutlich Chromium-only.
Mit Watch-Variablen komme ich einfach nicht richtig zurecht.
Scope und Aktualisierungsregeln sind viel zu undurchschaubar; man könnte meinen, nur globale Variablen seien überwachbar, aber selbst das funktioniert nicht wie erwartet, sodass ich beim Testen am Ende doch wieder Logs mit Werten vollschreibe.
Seit Jahren denke ich, man müsste eine UI im Stil von Data.gui [1] in die Konsole einbauen, damit man Variablen und Einstellungswerte ansehen und testen kann.
In diesem CodePen [2] sieht man das in Aktion.
Browser haben theoretisch viele großartige Debugging-Funktionen, aber in der Praxis scheinen sie nicht zuverlässig zu funktionieren.
Nicht einmal Breakpoints bekomme ich alle zuverlässig gesetzt.
Wenn der Code ungebündelt ist, ist es in Ordnung, aber sobald er gebündelt wird, scheinen viele Debugger-Funktionen kaputtzugehen, selbst wenn er nicht minifiziert wurde.
Ich bin kein Frontend Engineer, also mache ich vielleicht einfach irgendetwas falsch.
Sie sind wahrscheinlich die Ursache der Frustration, und ich verstehe immer noch nicht, warum sie standardmäßig aktiviert sind, obwohl die Erfahrung beim aktiven Debuggen damit so schlecht ist.
Es wäre gut, wenn es eine Möglichkeit gäbe, auf lokale Variablen einer IIFE zuzugreifen, ohne die Codeausführung innerhalb des IIFE-Scopes anzuhalten
Gibt es eine Möglichkeit, den Debugger dazu zu überreden?
https://en.m.wikipedia.org/wiki/Immediately_invoked_function...
Wenn man komplett außerhalb der IIFE ist, halte ich es für unmöglich
Die gesuchte Variable existiert vor oder nach der Ausführung möglicherweise gar nicht im Speicher
JavaScript hat im Gegensatz zu anderen Sprachen keine statischen Variablen, daher werden interne Variablen bei jedem Aufruf der IIFE verworfen
JavaScript hat, solange man nicht Dinge wie Web Worker verwendet, ein ziemlich starkes Single-Thread-Modell; wenn man also nicht mit
async/await-Aufrufen direkt Race Conditions erzeugen will, ist schon die Vorstellung schwierig, dass solche Variablen außerhalb des IIFE-Scopes im Speicher verbleibenWenn man
varmissbraucht und die Variable außerhalb des IIFE-Scopes initialisiert, kann man den Wert sehen, den der letzte IIFE-Aufruf erzeugt hatEs braucht ein bisschen Anpassung, aber der Nutzen im Verhältnis zum Aufwand ist groß
https://github.com/kristopolous/_inject
Man kann zu nahezu beliebigen Zeitpunkten durch fast jeden Funktionskontext wandern und sehen, was passiert ist
Es funktioniert, indem es Referenzzähler ausnutzt, damit Kontexte nicht zerstört werden
Als ich viel clientseitiges JS gemacht habe, war das wirklich nützlich
Die Killer-App-Version davon, richtig umgesetzt, wäre eine REPL, die man in jedem beliebigen Kontext öffnen kann
Schon im jetzigen Zustand braucht man einiges an Erfahrung, um es gut zu nutzen
https://firefox-source-docs.mozilla.org/devtools-user/debugg...
Bearbeitung: Mir ist gerade klar geworden, dass das buchstäblich das Erste ist, was im verlinkten Artikel behandelt wird
[[Scopes]]ansehenIn Firefox scheint es keine Möglichkeit dafür zu geben
Der Vollständigkeit halber kann ich Werkzeug empfehlen
Ich verwende es für Django-Backend-Entwicklung, und es ist enorm nützlich
Es stellt dir an jeder Stelle, an der eine Exception auftritt, direkt im Browser eine „PDB“-Shell zur Verfügung
1/0an die Stelle setzen muss, an der Werkzeug im Browser erscheinen sollEs ist sehr einfach und intuitiv
django-extension/runserver_plus?Ein Trick, den ich ständig nutze, ist, geladene Skripte nach UI-Strings zu durchsuchen, um zu debuggen
Gehe ins Network-Panel, starte die Aufzeichnung der Netzwerk-Requests, öffne die linke Seitenleiste und suche nach dem Code oder UI-String, den du finden willst
Meist findet man ihn in irgendeiner merkwürdigen gebündelten JS-Chunk-Datei; klickt man auf das Ergebnis, öffnet sich der Netzwerk-Request dieser Datei
Wenn man irgendwo in dieser Datei rechtsklickt und etwas wie „Open in Sources“ auswählt, landet man im Debugger
Jetzt kann man eine
debugger-Anweisung setzen, und dabei werden wahrscheinlich auch die Source Maps geladenIch arbeite seit Langem als Python/Elixir-Programmierer und habe viel
pdb.set_trace()undIEx.pry()genutztKürzlich habe ich ein sehr unordentliches NodeJS-Backend geerbt und reiße mir ohne vernünftige Debugging-Tools die letzten Haare aus
Ich bin wieder bei
console.log-Debugging gelandet, aber ich fühle mich wie ein HöhlenmenschIch kann kaum glauben, dass es in einem so beliebten Ökosystem keine ordentliche Debugging-REPL gibt; kann mir jemand sagen, wohin ich gehen sollte?
Verwende
node --inspect-brk; damit kannst du dich per VS Code oder Chrome DevTools verbindenDer knifflige Teil ist, ob Build-Tools wie TypeScript-Transpilation vor dem
nodejs-Befehl stehenWenn du einfach
nodeausführst, ist es ziemlich einfachDie Debugging-REPL ist die JavaScript-Konsole selbst
Wenn du es als Debugging-Option hinzufügst, kannst du Breakpoints setzen und in Funktionen hineinsteppen
Da das meiste in Objekten gekapselt ist, lässt sich JavaScript ziemlich gut debuggen
--inspectoder--inspect-brkIn VSCode kannst du auch den Befehl „open JavaScript debug terminal“ verwenden
Dieser Befehl öffnet ein Terminal, in dem jeder beliebige
node-Befehl automatisch mit angehängtem Debugger gestartet wirdhttps://pypi.org/project/snoop/
Das ist ein Decorator für tiefes Tracing
Im Node-JS-Ökosystem habe ich so eine Funktion nicht gesehen
Im Abschnitt „Debugging Property Reads“ frage ich mich, wie man mithilfe eines bedingten Breakpoints
{configOption: true}in{get configOption() { debugger; return true; }}umwandeltMan kann den Wert in der Konsole mit einem Getter überschreiben, und wenn man Schreibrechte hat, kann man ihn auch direkt im Quellcode ändern
Das sollte man wohl unbedingt lernen, aber ich frage mich, ob es dafür ein spezielles Buch oder Lernmaterial gibt.
Oder bleibt einem nichts anderes übrig, als sich endlos in Webentwicklung und Frontend zu vertiefen?
Wenn man gerade mitten in der Arbeit steckt, ist es vielleicht nicht verlockend, auf einen Link zu klicken, um Release Notes zu lesen, aber schon 5 Minuten Überfliegen bei jeder neuen Version lohnen sich.
Es gibt auch kurze, leicht verdauliche Videos von ein paar Minuten Länge, mit denen man einen schnellen Überblick bekommt.
Ziel ist zwar, neue Funktionen vorzustellen, aber meiner Erfahrung nach versteht man dabei oft auch die aktuellen Grenzen der Tools.