Fallstudie: Besseres Angular-Debugging mit Entwicklertools

Verbesserte Fehlerbehebung

In den letzten Monaten hat das Chrome-Entwicklertools-Team mit dem Angular-Team zusammengearbeitet, um die Fehlerbehebung in den Chrome-Entwicklertools zu verbessern. Die Teams beider Teams haben gemeinsam Maßnahmen ergriffen, um Entwicklern das Debuggen und Profilieren von Webanwendungen aus Autorenperspektive zu ermöglichen: in Bezug auf die Ausgangssprache und die Projektstruktur mit Zugriff auf Informationen, die ihnen vertraut und relevant sind.

In diesem Beitrag erfahren Sie, welche Änderungen in Angular und den Chrome-Entwicklertools erforderlich waren, um dies zu erreichen. Obwohl einige dieser Änderungen über Angular demonstriert werden, können sie auch auf andere Frameworks angewendet werden. Das Team der Chrome-Entwicklertools ermutigt auch andere Frameworks, die neuen Konsolen-APIs und Source Map-Erweiterungspunkte zu übernehmen, damit auch sie ihren Nutzern eine bessere Fehlerbehebung bieten können.

Code zum Ignorieren von Listen

Beim Debuggen von Anwendungen mit den Chrome-Entwicklertools möchten die Autoren in der Regel nur nur ihren Code sehen, nicht den des zugrunde liegenden Frameworks oder eine im Ordner node_modules versteckte Abhängigkeit.

Zu diesem Zweck hat das Entwicklertools-Team eine Erweiterung für Source Maps mit dem Namen x_google_ignoreList eingeführt. Diese Erweiterung wird verwendet, um Drittanbieterquellen wie Framework-Code oder Bundler-generierten Code zu identifizieren. Wenn ein Framework diese Erweiterung verwendet, vermeiden Autoren Code, den sie nicht sehen oder den sie nicht sehen möchten, ohne diesen vorher manuell konfigurieren zu müssen.

In der Praxis können die Chrome-Entwicklertools automatisch Code ausblenden, der in den Stacktraces, der Quellenstruktur oder dem Schnellzugriff-Dialogfeld erkannt wird. Außerdem können die Schritte und die Wiederaufnahme im Debugger verbessert werden.

Ein animiertes GIF, das die Entwicklertools davor und danach zeigt. Beachten Sie, dass die Entwicklertools im Nachher-Image den erstellten Code in der Baumstruktur und keine Framework-Dateien mehr im Menü „Quick Open“ vorschlagen und rechts einen übersichtlicheren Stacktrace sehen.

Quellzuordnungserweiterung x_google_ignoreList

In Quellzuordnungen verweist das neue Feld x_google_ignoreList auf das Array sources und listet die Indizes aller bekannten Drittanbieterquellen in dieser Quellzuordnung auf. Beim Parsen der Source Map wird in den Chrome-Entwicklertools ermittelt, welche Abschnitte des Codes ignoriert werden sollen.

Unten sehen Sie eine Source Map für die generierte Datei out.js. Es gibt zwei ursprüngliche sources, die zur Erstellung der Ausgabedatei beigetragen haben: foo.js und lib.js. Ersteres ist etwas, das ein Website-Entwickler geschrieben hat, und Letzteres ist ein Framework, das es verwendet hat.

{
  "version" : 3,
  "file": "out.js",
  "sourceRoot": "",
  "sources": ["foo.js", "lib.js"],
  "sourcesContent": ["...", "..."],
  "names": ["src", "maps", "are", "fun"],
  "mappings": "A,AAAB;;ABCDE;"
}

Die sourcesContent ist in beiden ursprünglichen Quellen enthalten und die Chrome-Entwicklertools würden diese Dateien standardmäßig im Debugger anzeigen:

  • Als Dateien in der Quellenstruktur.
  • Wie Ergebnisse im Schnellzugriff-Dialogfeld.
  • Als zugeordnete Aufrufframe-Positionen in Fehler-Stacktraces, während sie an einem Haltepunkt pausiert wurden und dabei Schritte ausgeführt werden.

Es gibt eine zusätzliche Information, die jetzt in Source Maps aufgenommen werden kann, um zu ermitteln, bei welcher dieser Quellen es sich um Erst- oder Drittanbietercode handelt:

{
  ...
  "sources": ["foo.js", "lib.js"],
  "x_google_ignoreList": [1],
  ...
}

Das neue Feld x_google_ignoreList enthält einen einzelnen Index, der auf das Array sources verweist: 1. Damit wird angegeben, dass die Regionen, die lib.js zugeordnet sind, Drittanbietercode sind, der automatisch zur Ignorierliste hinzugefügt werden sollte.

In einem komplexeren Beispiel, wie unten gezeigt, geben die Indexe 2, 4 und 5 an, dass die den lib1.ts, lib2.coffee und hmr.js zugeordneten Regionen Drittanbietercode sind, der automatisch der Ignorierliste hinzugefügt werden soll.

{
  ...
  "sources": ["foo.html", "bar.css", "lib1.ts", "baz.js", "lib2.coffee", "hmr.js"],
  "x_google_ignoreList": [2, 4, 5],
  ...
}

Wenn Sie ein Framework- oder Bundler-Entwickler sind, achten Sie darauf, dass die während des Build-Prozesses generierten Quellzuordnungen dieses Feld enthalten, damit die neuen Funktionen in den Chrome-Entwicklertools verwendet werden können.

x_google_ignoreList“ in Angular

Ab Angular-Version 14.1.0 wurde der Inhalt der Ordner node_modules und webpack als zu ignorieren gekennzeichnet.

Dies wurde durch eine Änderung bei angular-cli erreicht, indem ein Plug-in erstellt wurde, das sich in das Compiler-Modul von Webpack einbinden lässt

Das von unseren Entwicklern erstellte Webpack-Plug-in wird in die PROCESS_ASSETS_STAGE_DEV_TOOLING-Phase aufgenommen und füllt das Feld x_google_ignoreList in den Quellzuordnungen für die endgültigen Assets aus, die von Webpack generiert und im Browser geladen werden.

const map = JSON.parse(mapContent) as SourceMap;
const ignoreList = [];

for (const [index, path] of map.sources.entries()) {
  if (path.includes('/node_modules/') || path.startsWith('webpack/')) {
    ignoreList.push(index);
  }
}

map[`x_google_ignoreList`] = ignoreList;
compilation.updateAsset(name, new RawSource(JSON.stringify(map)));

Verknüpfte Stacktraces

Stacktraces beantworten die Frage „Wie bin ich hierhin gekommen“, oft ist dies jedoch aus der Perspektive des Computers und stimmt nicht unbedingt mit der Perspektive des Entwicklers oder seinem mentalen Modell der Anwendungslaufzeit überein. Dies gilt insbesondere, wenn einige Vorgänge später asynchron ausgeführt werden: Es könnte immer noch interessant sein, die „Ursache“ oder die Planungsseite solcher Vorgänge zu kennen, aber genau das ist nicht Teil eines asynchronen Stacktrace.

V8 verfügt intern über einen Mechanismus, mit dem solche asynchronen Aufgaben nachverfolgt werden können, wenn Standardprivilegien für die Browserplanung wie setTimeout verwendet werden. Dies geschieht standardmäßig in diesen Fällen, sodass die Entwickler diese bereits überprüfen können. Bei komplexeren Projekten ist dies jedoch nicht so einfach, insbesondere wenn ein Framework mit fortschrittlicheren Planungsmechanismen verwendet wird, z. B. eines, das Zonenverfolgung, benutzerdefinierte Aufgabenwarteschlangen ausführt oder Aktualisierungen in mehrere Arbeitseinheiten unterteilt, die im Laufe der Zeit ausgeführt werden.

Um dieses Problem zu beheben, stellt die Entwicklertools einen Mechanismus namens „Async Stack Tagging API“ für das console-Objekt bereit, mit dem Framework-Entwickler angeben können, wo und wo Vorgänge geplant sind.

Die Async Stack Tagging API

Ohne Async Stack Tagging werden Stacktraces für Code, der von Frameworks asynchron auf komplexe Weise ausgeführt wird, ohne Verbindung zum Code angezeigt, für den er geplant wurde.

Ein Stacktrace mit einem asynchron ausgeführten Code ohne Informationen darüber, wann er geplant wurde. Es wird nur der Stacktrace ab `requestAnimationFrame` angezeigt, enthält aber keine Informationen vom Zeitpunkt der Planung.

Mit Async Stack Tagging ist es möglich, diesen Kontext anzugeben. Der Stacktrace sieht dann so aus:

Ein Stacktrace eines asynchron ausgeführten Codes mit Informationen darüber, wann er geplant wurde. Beachten Sie, dass darin im Stacktrace „businessLogic“ und „schedule“ enthalten sind.

Verwenden Sie dazu die neue console-Methode console.createTask(), die von der Async Stack Tagging API bereitgestellt wird. Die Signatur lautet wie folgt:

interface Console {
  createTask(name: string): Task;
}

interface Task {
  run<T>(f: () => T): T;
}

Wenn Sie console.createTask() aufrufen, wird eine Task-Instanz zurückgegeben, die Sie später zum Ausführen des asynchronen Codes verwenden können.

// Task Creation
const task = console.createTask(name);

// Task Execution
task.run(f);

Die asynchronen Vorgänge können auch verschachtelt sein. Die Grundursachen werden dann der Reihe nach im Stacktrace angezeigt.

Aufgaben können beliebig oft ausgeführt werden und die Arbeitslast kann sich bei den einzelnen Ausführungen unterscheiden. Der Aufrufstack auf der Planungswebsite wird gespeichert, bis das Aufgabenobjekt automatisch bereinigt wird.

Die Async Stack Tagging API in Angular

In Angular wurden Änderungen an NgZone vorgenommen – dem Ausführungskontext von Angular, der über asynchrone Aufgaben hinweg bestehen bleibt.

Bei der Planung einer Aufgabe wird console.createTask() verwendet, sofern verfügbar. Die resultierende Task-Instanz wird für die weitere Verwendung gespeichert. Beim Aufrufen der Aufgabe verwendet NgZone die gespeicherte Instanz Task, um sie auszuführen.

Diese Änderungen landeten über die Pull-Anfragen #46693 und #46958 in der NgZone 0.11.8 von Angular.

Freundliche Call-Frames

Frameworks generieren beim Erstellen eines Projekts oft Code aus allen Arten von Vorlagensprachen, z. B. Angular- oder JSX-Vorlagen, die HTML-Code in einfachen JavaScript-Code umwandeln, der schließlich im Browser ausgeführt wird. Manchmal erhalten diese Arten von generierten Funktionen Namen, die nicht sehr ansprechend sind – entweder Namen mit einzelnen Buchstaben, nachdem sie reduziert wurden, oder unklare oder unbekannte Namen, selbst wenn dies nicht der Fall ist.

In Angular ist es nicht ungewöhnlich, dass Aufrufframes mit Namen wie AppComponent_Template_app_button_handleClick_1_listener in Stacktraces angezeigt werden.

Screenshot des Stacktrace mit einem automatisch generierten Funktionsnamen.

Aus diesem Grund unterstützt die Chrome-Entwicklertools das Umbenennen dieser Funktionen mithilfe von Source Maps. Wenn eine Quellzuordnung einen Namenseintrag für den Anfang eines Funktionsbereichs hat (d. h. den linken Klammer der Parameterliste), sollte im Aufrufframe dieser Name im Stacktrace angezeigt werden.

Freundliche Call-Frames in Angular

Das Umbenennen von Callframes in Angular ist ein kontinuierlicher Prozess. Wir gehen davon aus, dass diese Verbesserungen im Laufe der Zeit schrittweise umgesetzt werden.

Beim Parsen der von Autoren geschriebenen HTML-Vorlagen generiert der Angular-Compiler TypeScript-Code, der schließlich in JavaScript-Code umgewandelt wird, den der Browser lädt und ausführt.

Im Rahmen dieses Prozesses der Codegenerierung werden auch Source Maps erstellt. Wir untersuchen derzeit Möglichkeiten, Funktionsnamen in das Feld „names“ von Source Maps aufzunehmen und auf diese Namen in den Zuordnungen zwischen dem generierten Code und dem Originalcode zu verweisen.

Wenn beispielsweise eine Funktion für einen Event-Listener generiert wird und ihr Name während der Reduzierung entweder unfreundlich ist oder entfernt wird, können Quellzuordnungen jetzt den nutzerfreundlicheren Namen für diese Funktion im Feld „names“ enthalten. Die Zuordnung für den Anfang des Funktionsbereichs kann sich nun auf diesen Namen beziehen (d. h. auf die linke Klammer der Parameterliste). Die Chrome-Entwicklertools verwenden diese Namen dann, um Aufrufframes in Stacktraces umzubenennen.

Zukunftspläne

Es war eine wunderbare Erfahrung, Angular als Testpilot zu verwenden, um unsere Arbeit zu überprüfen. Wir freuen uns über Feedback von Framework-Entwicklern und Feedback zu diesen Erweiterungspunkten.

Es gibt noch weitere Bereiche, die wir gerne erkunden würden. Insbesondere, wie die Profilerstellung in den Entwicklertools verbessert werden kann.