Studium przypadku: Better Angular Debugging with DevTools

Ulepszone debugowanie

W ciągu ostatnich kilku miesięcy zespół Narzędzi deweloperskich w Chrome współpracował z zespołem Angulara nad ulepszeniami debugowania w Narzędziach deweloperskich w Chrome. Pracownicy obu zespołów współpracowali ze sobą i podejmowali działania, aby umożliwić deweloperom debugowanie i profilowanie aplikacji internetowych z perspektywy autora: w języku źródłowym i strukturze projektu, z dostępem do informacji, które są im znane i przydatne.

W tym poście przyjrzymy się bliżej temu, jakie zmiany w Angular i Narzędziach deweloperskich w Chrome były potrzebne do osiągnięcia tego celu. Chociaż niektóre z tych zmian są demonstrowane na przykładzie Angulara, można je zastosować również w innych ramach. Zespół Chrome DevTools zachęca inne frameworki do korzystania z nowych interfejsów API konsoli i punktów rozszerzenia mapy źródłowej, aby również one mogły oferować użytkownikom lepsze możliwości debugowania.

Kod ignorowania wizytówki

Podczas debugowania aplikacji za pomocą Narzędzi deweloperskich w Chrome autorzy zwykle chcą widzieć tylko swój kod, a nie frameworku lub zależności ukrytej w folderze node_modules.

Aby to osiągnąć, zespół Narzędzi deweloperskich wprowadził rozszerzenie map źródeł o nazwę x_google_ignoreList. To rozszerzenie służy do identyfikowania źródeł zewnętrznych, takich jak kod frameworku lub kod wygenerowany przez pakiet. Gdy framework używa tego rozszerzenia, autorzy mogą automatycznie unikać kodu, którego nie chcą widzieć lub przez który nie chcą przechodzić bez konieczności ręcznej konfiguracji.

W praktyce Narzędzia deweloperskie Chrome mogą automatycznie ukrywać kod zidentyfikowany w śladach stosu, drzewie źródeł i oknie Szybkie otwieranie, a także poprawiać działanie funkcji krokowania i wznawiania w debugerze.

Animowany GIF pokazujący DevTools przed i po. Zwróć uwagę, że na obrazie „Po” w Narzędziach deweloperskich widać w drzewie kod autorski, nie ma już sugestii dotyczących plików frameworku w menu „Otwórz szybko” i po prawej stronie jest znacznie czystszy ślad stosu.

Rozszerzenie mapy źródeł x_google_ignoreList

W mapach źródeł nowe pole x_google_ignoreList odwołuje się do tablicy sources i wypisuje indeksy wszystkich znanych źródeł zewnętrznych w danym pliku mapy źródeł. Podczas analizowania mapy źródłowej Narzędzia deweloperskie w Chrome użyją tego parametru, aby ustalić, które sekcje kodu należy pominąć.

Poniżej znajduje się mapa źródłowa wygenerowanego pliku out.js. Do wygenerowania pliku wyjściowego przyczyniły się 2 pierwotne sources: foo.jslib.js. Pierwsza to część napisana przez dewelopera witryny, a druga to framework, którego użył.

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

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

  • jako pliki w drzewie źródeł.
  • W oknie Szybkie otwieranie.
  • Jako mapowane lokalizacje wywołań w ramkach błędów w wyświetleniu ścieżki błędów podczas wstrzymywania w miejscu punktu przerwania i przechodzenia.

W mapach źródeł można teraz uwzględnić 1 dodatkowy element informacji, który pozwala określić, które z nich są kodem własnym, a które kodem zewnętrznym:

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

Nowe pole x_google_ignoreList zawiera pojedynczy indeks odwołujący się do tablicy sources: 1. Określa to, że regiony mapowane na lib.js są w istocie kodem zewnętrznym, który powinien być automatycznie dodawany do listy ignorowanych.

W bardziej złożonym przykładzie pokazanym poniżej wskaźniki 2, 4 i 5 wskazują, że regiony mapowane na lib1.ts, lib2.coffeehmr.js to kody innych firm, które powinny być automatycznie dodawane do listy ignorowanych.

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

Jeśli jesteś deweloperem frameworka lub pakietu, upewnij się, że mapy źródeł wygenerowane podczas procesu kompilacji zawierają to pole, aby można było korzystać z tych nowych funkcji w narzędziach programistycznych Chrome.

x_google_ignoreList w Angular

Od wersji Angular 14.1.0 zawartość folderów node_moduleswebpack została oznaczona jako „do zignorowania”.

Osiągnięto to przez zmianę w angular-cli, tworząc wtyczkę, która łączy się z modułem Compiler w webpacku.

Wtyczka webpack, którą nasi inżynierowie stworzyli na etapie PROCESS_ASSETS_STAGE_DEV_TOOLING, wypełnia pole x_google_ignoreList w mapach źródeł dla końcowych zasobów, które webpack generuje i które przeglądarka wczytuje.

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

Ścieżki stosu odpowiadają na pytanie „jak się tu znalazłem”, ale często jest to widok z perspektywy maszyny i niekoniecznie odpowiada on postrzeganiu dewelopera lub jego mentalnemu modelowi działania aplikacji. Jest to szczególnie ważne, gdy niektóre operacje są zaplanowane do wykonania asynchronicznie w późniejszym czasie: nadal może być interesujące poznanie „przyczyny źródłowej” lub strony planowania takich operacji, ale nie będzie to widoczne w asynchronicznym śladzie stosu.

V8 ma wewnętrzny mechanizm śledzenia takich zadań asynchronicznych, gdy używane są standardowe prymitywy harmonogramowania w przeglądarce, takie jak setTimeout. W takich przypadkach jest to domyślne, więc deweloperzy mogą już je sprawdzić. W przypadku bardziej złożonych projektów nie jest to jednak takie proste, zwłaszcza gdy używasz platformy z bardziej zaawansowanymi mechanizmami harmonogramowania, np. takiej, która wykonuje śledzenie stref, kolejkowanie niestandardowych zadań lub dzieli aktualizacje na kilka jednostek pracy, które są wykonywane w czasie.

Aby rozwiązać ten problem, DevTools udostępnia mechanizm o nazwie „Interfejs API otagowania stosu asynchronicznego” w obiekcie console, który umożliwia twórcom frameworków wskazywanie lokalizacji, w których zaplanowano operacje, oraz lokalizacji, w których są one wykonywane.

Async Stack Tagging API

Bez oznaczenia kodu asynchronicznego zrzuty kodu w przypadku kodu, który jest asynchronicznie wykonywany w złożony sposób przez frameworky, pojawiają się bez związku z kodem, w którym został zaplanowany.

ślad stosu dla kodu wykonanego asynchronicznie bez informacji o tym, kiedy został zaplanowany; Pokazuje tylko ślad stosu od metody requestAnimationFrame, ale nie zawiera informacji o czasie jego zaplanowania.

Dzięki asynchronicznemu tagowaniu stosu można podać ten kontekst, a zrzut stosu będzie wyglądał tak:

ślad stosu niektórych asynchronicznie wykonywanych fragmentów kodu z informacjami o czasie ich zaplanowania. Zwróć uwagę, że w odróżnieniu od poprzedniego śladu stosu zawiera on metody „businessLogic” i „schedule”.

Aby to zrobić, użyj nowej metody console o nazwie console.createTask(), którą udostępnia interfejs API tagowania za pomocą asynkronicznej warstwy. Jego podpis wygląda tak:

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 mogą być również zagnieżdżone, a „przyczyny źródłowe” będą wyświetlane w kolejności w wyświetleniu ścieżki wywołań.

Zadania można uruchamiać dowolną liczbę razy, a ładunek roboczy może się różnić w każdym uruchomieniu. Stos wywołań w miejscu planowania będzie zapamięty, dopóki obiekt zadania nie zostanie usunięty.

Async Stack Tagging API w Angular

W Angular wprowadziliśmy zmiany w NgZone – kontekście wykonania w Angular, który jest trwały w przypadku zadań asynchronicznych.

Podczas planowania zadania używa console.createTask(), jeśli jest dostępny. Wynikowa instancja Task jest przechowywana do dalszego użycia. Po wywołaniu zadania NgZone użyje do jego wykonania zapisanej instancji Task.

Te zmiany zostały wprowadzone w wersji 0.11.8 interfejsu NgZone w ramach pull request #46693#46958.

Ramki przyjaznych rozmów

Podczas tworzenia projektu frameworki często generują kod z różnych języków szablonów, takich jak Angular czy szablony JSX, które przekształcają kod podobny do kodu HTML w zwykły kod JavaScript, który ostatecznie działa w przeglądarce. Czasami tego typu wygenerowane funkcje mają nazwy, które nie są zbyt przyjazne – albo nazwy jednoliterowe po skompresowaniu lub niejasne lub nieznane nazwy, nawet jeśli nie są skompresowane.

W przypadku Angulara w śladach stosu często można zobaczyć ramki wywołania o nazwach takich jak AppComponent_Template_app_button_handleClick_1_listener.

Zrzut ekranu ścieżki wywołania z automatycznie wygenerowaną nazwą funkcji

Aby rozwiązać ten problem, narzędzia deweloperskie w Chrome obsługują teraz zmianę nazwy tych funkcji za pomocą map źródłowych. Jeśli mapa źródłowa zawiera wpis z nazwą początku zakresu działania funkcji (czyli lewą nawiasem na liście parametrów), ramka wywołania powinna wyświetlać tę nazwę w wyświetleniu stosu.

Ramki wywołania przyjazne dla użytkownika w Angular

Zmiana nazw ramek wywołania w Angular to ciągły proces. 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 następnie przekształcany w kod JavaScript, który przeglądarka wczytuje i uruchamia.

W ramach tego procesu generowania kodu są też tworzone mapy źródeł. Obecnie szukamy sposobów na uwzględnianie nazw funkcji w polu „names” map źródłowych i odwoływania się do tych nazw w mapowaniu między wygenerowanym kodem a oryginalnym kodem.

Jeśli na przykład wygenerowana zostanie funkcja dla odbiornika zdarzeń, a jej nazwa zostanie usunięta lub zmieniona na nieprzyjazną podczas kompresji, mapy źródłowe mogą teraz zawierać w polu „names” bardziej przyjazną nazwę tej funkcji, a mapowanie na początku zakresu funkcji może się do niej odwoływać (czyli do lewej nawiasu w liście parametrów). Narzędzia deweloperskie w Chrome będą używać tych nazw do zmiany nazw ramek wywołania w śladach stosu.

Perspektywy

Korzystanie z Angular w ramach programu pilotażowego, aby sprawdzić naszą pracę, było wspaniałym doświadczeniem. Chętnie poznamy opinie deweloperów frameworków na temat tych punktów rozszerzeń. Prześlij nam swoją opinię.

Jest jeszcze wiele obszarów, które chcielibyśmy zbadać. W szczególności chodzi o to, jak ulepszyć profilowanie w Narzędziach deweloperskich.