Studium przypadku: Better Angular Debugging with DevTools

Ulepszone debugowanie

W ciągu ostatnich kilku miesięcy zespół Narzędzi deweloperskich Chrome współpracował z zespołem Angular, aby wprowadzić ulepszenia w zakresie debugowania w Narzędziach deweloperskich w Chrome. Osoby z obu zespołów pracowały nad tym, by umożliwić deweloperom debugowanie i profilowanie aplikacji internetowych z perspektywy twórców: pod względem języka źródłowego i struktury projektów oraz dostępu do informacji, które były im znane i istotne.

W tym poście szczegółowo opisujemy, jakie zmiany w Angular i Narzędziach deweloperskich w Chrome były niezbędne, aby osiągnąć ten efekt. Chociaż niektóre z tych zmian pokazujemy w Angular, można je również zastosować w innych środowiskach. Zespół Narzędzi deweloperskich w Chrome zachęca inne platformy do stosowania nowych interfejsów API konsoli i punktów rozszerzenia mapy źródeł, aby one również mogły zapewnić użytkownikom lepsze środowisko debugowania.

Kod ignorowania informacji

Podczas debugowania aplikacji przy użyciu Narzędzi deweloperskich w Chrome autorzy chcą mieć dostęp tylko do swojego kodu, a nie platformy znajdującej się pod nim lub niektórych zależności ukrytych w folderze node_modules.

W tym celu zespół Narzędzi deweloperskich wprowadził rozszerzenie map źródłowych o nazwie x_google_ignoreList. To rozszerzenie służy do identyfikowania źródeł zewnętrznych, takich jak kod platformy lub kod wygenerowany przez kreatora pakietów. Gdy platforma korzysta z tego rozszerzenia, autorzy automatycznie unikają teraz kodu, który nie chce zobaczyć ani przejść przez niego bez konieczności ręcznego konfigurowania tego rozszerzenia.

W praktyce Narzędzia deweloperskie w Chrome mogą automatycznie ukrywać kod zidentyfikowany na przykład w zrzutach stosu, w drzewie Źródła i oknie Szybkie otwieranie, a także usprawniają działanie kroków i wznawiania debugera.

Animowany GIF przedstawiający Narzędzia deweloperskie przed i po. Na obrazie po obrazie w Narzędziach deweloperskich widać, jak w drzewie jest utworzony kod, nie sugerują już żadnych plików platformy w menu „Szybkie otwieranie”, a z prawej strony widać znacznie bardziej przejrzysty zrzut stosu.

Rozszerzenie mapy źródłowej x_google_ignoreList

W mapach źródłowych nowe pole x_google_ignoreList odnosi się do tablicy sources i zawiera indeksy wszystkich znanych źródeł zewnętrznych w tej mapie. Podczas analizowania mapy źródłowej Narzędzia deweloperskie w Chrome na podstawie tego będą określać, które sekcje kodu należy wykluczyć z listy.

Poniżej znajdziesz mapę źródeł dla wygenerowanego pliku out.js. Istnieją 2 pierwotne elementy sources, które miały udział w wygenerowaniu pliku wyjściowego: foo.js i lib.js. Pierwszy to tekst napisany przez dewelopera witryny, a drugi to platforma, której on używa.

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

Parametr sourcesContent jest uwzględniony w obu tych źródłach, a Narzędzia deweloperskie w Chrome domyślnie wyświetlają te pliki w Debugerze:

  • Jako pliki w drzewie Źródła.
  • W oknie Szybkie otwieranie.
  • Jako zmapowane lokalizacje ramek wywołań w zrzutach stosu błędów podczas wstrzymania na punkcie przerwania i podczas przechodzenia.

Teraz w mapach źródeł możesz uwzględnić jedną dodatkową informację, która pomoże Ci stwierdzić, które z nich jest kodem własnym, a które innej firmy:

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

Nowe pole x_google_ignoreList zawiera jeden indeks odnoszący się do tablicy sources: 1. Oznacza to, że regiony zmapowane na lib.js są w rzeczywistości kodem innej firmy, który powinien zostać automatycznie dodany do listy ignorowanych.

W bardziej złożonym przykładzie pokazanym poniżej indeksy 2, 4 i 5 wskazują, że regiony zmapowane na lib1.ts, lib2.coffee i hmr.js to kod innej firmy, który powinien zostać automatycznie dodany do listy ignorowanych.

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

Jeśli tworzysz platformy lub pakiety SDK, upewnij się, że mapy źródłowe generowane podczas procesu kompilacji zawierają to pole, aby korzystać z tych nowych możliwości w Narzędziach deweloperskich w Chrome.

x_google_ignoreList w Angular

Od Angular w wersji 14.1.0 zawartość folderów node_modules i webpack jest oznaczona jako „do ignorowania”.

Jest to możliwe dzięki zmianie w angular-cli polegającej na utworzeniu wtyczki, która łączy się z modułem Compiler pakietu internetowego.

Opracowana przez naszych inżynierów wtyczka webpack łączy się z etapem PROCESS_ASSETS_STAGE_DEV_TOOLING i wypełnia pole x_google_ignoreList w mapach źródłowych dla ostatecznych zasobów generowanych i wczytywanych przez przeglądarkę.

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)));

Połączone zrzuty stosu

Zrzuty stosu pozwalają odpowiedzieć na pytanie, „jak udało mi się tu dotrzeć”, ale często są one widoczne z perspektywy komputera i niekoniecznie pasują do punktu widzenia dewelopera lub jego modelu psychicznego środowiska wykonawczego aplikacji. Jest to szczególnie istotne, gdy niektóre operacje zostaną zaplanowane asynchronicznie później: nadal interesujące może być poznanie „głównej przyczyny” lub planowania takich operacji, ale nie jest to częścią asynchronicznego zrzutu stosu.

V8 ma wewnętrznie mechanizm do śledzenia takich asynchronicznych zadań w przypadku użycia standardowych podstawowych elementów planowania przeglądarki, np. setTimeout. W takich przypadkach jest to domyślnie wykonywane, aby deweloperzy mogli już je sprawdzić. Jednak w bardziej złożonych projektach to nie jest tak proste, zwłaszcza gdy używasz platformy z bardziej zaawansowanymi mechanizmami planowania – na przykład takiej, która śledzi strefy lub niestandardowe kolejki zadań lub dzielisz aktualizacje na kilka jednostek wykonywanych na przestrzeni czasu.

Aby rozwiązać ten problem, w Narzędziach deweloperskich udostępniamy w obiekcie console mechanizm o nazwie „Async Stack Tagging API”. Dzięki temu programiści platformy mogą wskazywać zarówno lokalizacje, w których są planowane działania, jak i te, w których są one wykonywane.

Async Stack Tagging API

Bez tagowania stosu asynchronicznego tagi stosu kodu, który jest asynchronicznie wykonywany przez platformy w złożony sposób, pojawiają się bez połączenia z kodem, w którym został zaplanowany.

Zrzut stosu pewnego wykonanego kodu asynchronicznego bez informacji o tym, kiedy został zaplanowany. Pokazuje tylko zrzut stosu zaczynający się od „requestAnimationFrame”, ale nie zawiera żadnych informacji pochodzących od czasu jego planowania.

W przypadku tagowania stosu asynchronicznego można podać taki kontekst, a zrzut stosu wygląda tak:

Zrzut stosu niektórych wykonanego kodu asynchronicznego z informacjami o tym, kiedy został zaplanowany. Zwróć uwagę, że w przeciwieństwie do poprzedniego zrzutu stosu zawiera on elementy „businessLogic” i „schedule” (harmonogram).

Aby to zrobić, użyj nowej metody console o nazwie console.createTask(), którą udostępnia interfejs Async Stack Tagging API. Jego podpis jest następujący:

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

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

Wywołanie console.createTask() zwraca instancję Task, której można później użyć do uruchomienia kodu asynchronicznego.

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

// Task Execution
task.run(f);

Operacje asynchroniczne również mogą być zagnieżdżone, a „główne przyczyny” będą wyświetlane w zrzucie stosu po kolei.

Zadania można uruchamiać dowolną liczbę razy, a ładunek roboczy może się różnić w zależności od każdego uruchomienia. Stos wywołań w witrynie harmonogramu będzie zapamiętywany do momentu, gdy obiekt zadania nie zostanie odtworzony do czyszczenia pamięci.

Interfejs Async Stack Tagging API w Angular

W Angular wprowadzono zmiany w NgZone – kontekście wykonywania Angular, który pozostaje w zadaniach asynchronicznych.

Podczas planowania zadania używa ono funkcji console.createTask(), gdy jest dostępna. Powstała w ten sposób instancja Task jest przechowywana do dalszego użycia. Po wywołaniu zadania NgZone użyje do jego uruchomienia zapisanej instancji Task.

Te zmiany zostały wprowadzone w sekcji NgZone 0.11.8 w Angular w ramach żądań pull #46693 i #46958.

Przyjazne ramki wywołania

Podczas tworzenia projektu platformy często generują kod z różnych języków szablonów – na przykład szablonów Angular lub JSX, które zmieniają wygląd kodu HTML w zwykły kod JavaScript, który z czasem uruchamia się w przeglądarce. Czasami tego rodzaju generowane funkcje są nazywane nazwami, które nie są zbyt przyjazne. Mogą to być nazwy jednoliterowe po zminimalizacji albo niektóre niejasne lub nieznane nazwy, nawet jeśli nie są wcale.

W Angular nierzadko można zauważyć w zrzutach stosu ramki wywołań o nazwach takich jak AppComponent_Template_app_button_handleClick_1_listener.

Zrzut ekranu ze zrzutem stosu z automatycznie wygenerowaną nazwą funkcji.

Aby rozwiązać ten problem, w Narzędziach deweloperskich w Chrome możesz teraz zmieniać nazwy tych funkcji za pomocą map źródłowych. Jeśli mapa źródłowa zawiera wpis nazwy początku zakresu funkcji (czyli lewy nawias listy parametrów), ramka wywołania powinna wyświetlać tę nazwę w zrzucie stosu.

Przyjazne ramki wywołania w Angular

Zmiana nazw ramek wywołań w Angular jest ciągłym zadaniem. Spodziewamy się, że te ulepszenia będą wprowadzane stopniowo.

Podczas analizowania szablonów HTML napisanych przez autorów kompilator Angular generuje kod TypeScript, który jest ostatecznie przekształcany w kod JavaScript, który jest wczytywany i uruchamiany przez przeglądarkę.

W ramach tego procesu generowania kodu są również tworzone mapy źródłowe. Obecnie analizujemy sposoby uwzględniania nazw funkcji w polu „names” map źródłowych i odwoływania się do tych nazw w mapowaniach między wygenerowanym a oryginalnym kodem.

Jeśli na przykład funkcja detektora zdarzeń zostanie wygenerowana i jej nazwa będzie nieprzyjazna lub usunięta podczas minifikacji, mapy źródłowe mogą teraz zawierać bardziej przyjazną nazwę w polu „names”, a mapowanie początku zakresu funkcji może się odwoływać do tej nazwy (czyli w lewym nawiasie listy parametrów). Będą one używać tych nazw do zmiany nazw ramek wywołań w zrzutach stosu.

Perspektywy

Korzystanie z Angular w testach pilotażowych w celu sprawdzenia naszej pracy było dla nas niesamowitym doświadczeniem. Chętnie poznamy opinie deweloperów platformy i przekażemy opinię na temat tych punktów rozszerzeń.

Jest więcej obszarów, które chcemy zbadać. W szczególności chodzi o ulepszanie profilowania w Narzędziach deweloperskich.