Case study: migliore debug Angular con DevTools

Un'esperienza di debug migliorata

Negli ultimi mesi, il team di Chrome DevTools ha collaborato con il team di Angular per lanciare miglioramenti dell'esperienza di debug in Chrome DevTools. I membri di entrambi i team hanno lavorato insieme e hanno adottato misure per consentire agli sviluppatori di eseguire il debug e la profilazione delle applicazioni web dal punto di vista della creazione, in termini di lingua di origine e struttura del progetto, con accesso a informazioni familiari e pertinenti.

In questo post diamo un'occhiata in dettaglio per scoprire quali modifiche in Angular e Chrome DevTools sono state necessarie per raggiungere questo obiettivo. Anche se alcune di queste modifiche sono state dimostrate con Angular, possono essere applicate anche ad altri framework. Il team di Chrome DevTools incoraggia altri framework ad adottare le nuove API della console e i punti di estensione delle mappe di origine in modo che anche loro possano offrire una migliore esperienza di debug ai propri utenti.

Codice per ignorare la scheda

Durante il debug delle applicazioni con Chrome DevTools, gli autori di solito vogliono vedere solo solo il proprio codice, non quello del framework sottostante o alcune dipendenze nascoste nella cartella node_modules.

A questo scopo, il team DevTools ha introdotto un'estensione per le mappe di origine, chiamata x_google_ignoreList. Questa estensione viene utilizzata per identificare origini di terze parti come il codice del framework o il codice generato dal bundler. Quando un framework utilizza questa estensione, gli autori ora evitano automaticamente il codice che non vogliono vedere o seguire, senza doverlo configurare manualmente in anticipo.

In pratica, Chrome DevTools può nascondere automaticamente il codice identificato come tale nelle analisi dello stack, nell'albero delle origini e nella finestra di dialogo di apertura rapida, nonché migliorare il comportamento passo-passo e ripresa nel debugger.

Una GIF animata che mostra DevTools prima e dopo. Tieni presente che nell'immagine successiva DevTools mostra il codice creato dall'albero, non suggerisce più alcun file framework nel menu "Apertura rapida" e mostra un'analisi dello stack molto più pulita sulla destra.

L'estensione della mappa di origine x_google_ignoreList

Nelle mappe di origine, il nuovo campo x_google_ignoreList fa riferimento all'array sources ed elenca gli indici di tutte le origini di terze parti note in quella mappa di origine. Durante l'analisi della mappa di origine, Chrome DevTools lo utilizzerà per capire quali sezioni del codice devono essere incluse nell'elenco di elementi da ignorare.

Di seguito è riportata una mappa di origine per un file generato out.js. Sono presenti due sources originali che hanno contribuito a generare il file di output: foo.js e lib.js. Il primo è scritto da uno sviluppatore di siti web, mentre il secondo è un framework utilizzato dallo sviluppatore.

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

sourcesContent è incluso per entrambe le origini originali e Chrome DevTools mostrerà questi file per impostazione predefinita in Debugger:

  • Come file nell'albero delle origini.
  • Come risultati nella finestra di dialogo Apertura rapida.
  • Come posizioni dei frame delle chiamate mappate nelle analisi dello stack di errore durante la pausa in corrispondenza di un punto di interruzione e durante l'esecuzione dei passaggi.

Ora è possibile includere un'informazione aggiuntiva nelle mappe di origine per identificare quale di queste origini è codice proprietario o di terze parti:

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

Il nuovo campo x_google_ignoreList contiene un singolo indice che fa riferimento all'array sources: 1. In questo modo viene specificato che le regioni mappate a lib.js sono codici di terze parti che devono essere aggiunti automaticamente all'elenco di elementi da ignorare.

In un esempio più complesso, mostrato di seguito, gli indici 2, 4 e 5 specificano che le regioni mappate a lib1.ts, lib2.coffee e hmr.js sono tutti codici di terze parti che devono essere aggiunti automaticamente all'elenco di elementi da ignorare.

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

Se sei uno sviluppatore di framework o bundle, assicurati che le mappe di origine generate durante il processo di compilazione includano questo campo per collegarti a queste nuove funzionalità in Chrome DevTools.

x_google_ignoreList in Angular

A partire dalla versione Angular 14.1.0, i contenuti delle cartelle node_modules e webpack sono stati contrassegnati come "da ignorare".

Ciò è stato possibile tramite una modifica in angular-cli tramite la creazione di un plug-in che si collega al modulo Compiler del webpack.

Il plug-in webpack creato dai nostri tecnici esegue hook nella fase PROCESS_ASSETS_STAGE_DEV_TOOLING e compila il campo x_google_ignoreList nelle mappe di origine per gli asset finali generati dal webpack e caricato dal browser.

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

Analisi dello stack collegate

Le analisi dello stack rispondono alla domanda "come sono arrivato qui", ma spesso questo avviene dal punto di vista della macchina e non necessariamente dal punto di vista dello sviluppatore o dal suo modello mentale di runtime dell'applicazione. Questo vale soprattutto quando alcune operazioni sono programmate per essere eseguite in un secondo momento in modo asincrono: potrebbe comunque essere interessante conoscere la "causa principale" o il lato pianificazione di queste operazioni, ma è esattamente qualcosa che non farà parte di un'analisi dello stack asincrona.

V8 internamente dispone di un meccanismo per tenere traccia di queste attività asincrone quando vengono utilizzate primitive di pianificazione del browser standard, come setTimeout. In questi casi questa operazione viene eseguita per impostazione predefinita, quindi gli sviluppatori possono già ispezionarli. Ma nei progetti più complessi, questo non è così semplice, soprattutto quando si utilizza un framework con meccanismi di pianificazione più avanzati, ad esempio uno che esegue il monitoraggio delle zone, l'inserimento di code di attività personalizzate o che suddivide gli aggiornamenti in più unità di lavoro eseguite nel tempo.

Per risolvere questo problema, DevTools espone un meccanismo chiamato "API Async Stack Tagging" sull'oggetto console, che consente agli sviluppatori di framework di suggerire sia le località in cui vengono pianificate le operazioni sia dove vengono eseguite queste operazioni.

L'API Async Stack Tagging

Senza il tagging dello stack asincrono, le analisi dello stack per il codice eseguito in modo complesso dai framework in modo asincrono vengono visualizzate senza connessione al codice in cui è stato pianificato.

Un'analisi dello stack di codice eseguito in modo asincrono senza informazioni su quando è stato pianificato. Mostra solo l'analisi dello stack a partire da "requestAnimationFrame", ma non contiene informazioni a partire dalla data di pianificazione.

Con il tagging dello stack asincrono è possibile fornire questo contesto, e l'analisi dello stack ha il seguente aspetto:

Un'analisi dello stack di codice eseguito in modo asincrono con informazioni sulla data della pianificazione. Nota come, a differenza di prima, include "businessLogic" e "schedule" nell'analisi dello stack.

A questo scopo, utilizza un nuovo metodo console denominato console.createTask() fornito dall'API Async Stack Tagging. La sua firma è la seguente:

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

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

La chiamata a console.createTask() restituisce un'istanza Task, che puoi utilizzare in seguito per eseguire il codice asincrono.

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

// Task Execution
task.run(f);

Anche le operazioni asincrone possono essere nidificate e le "cause principali" verranno visualizzate nell'analisi dello stack in sequenza.

Le attività possono essere eseguite un numero qualsiasi di volte e il payload di lavoro può variare da un'esecuzione all'altra. Lo stack di chiamate nel sito di pianificazione verrà memorizzato finché l'oggetto dell'attività non viene garbage collection.

L'API Async Stack Tagging in Angular

In Angular sono state apportate modifiche a NgZone, il contesto di esecuzione di Angular che persiste in tutte le attività asincrone.

Quando pianifica un'attività, utilizza console.createTask(), se disponibile. L'istanza Task risultante viene archiviata per un ulteriore utilizzo. Dopo aver richiamato l'attività, NgZone utilizzerà l'istanza Task archiviata per eseguirla.

Queste modifiche sono arrivate nella NgZone 0.11.8 di Angular tramite richieste di pull #46693 e #46958.

Cornici per una chiamata

Durante la creazione di un progetto, i framework spesso generano codice da tutti i tipi di linguaggi di modello durante la creazione di un progetto, ad esempio i modelli Angular o JSX che trasformano il codice HTML in un semplice codice JavaScript che viene eseguito nel browser. A volte, questi tipi di funzioni generate hanno nomi poco amichevoli, come quelli con una sola lettera dopo averli minimizzati o alcuni nomi poco chiari o sconosciuti anche quando non lo sono.

Non è raro vedere frame di chiamata con nomi come AppComponent_Template_app_button_handleClick_1_listener nelle analisi dello stack in Angular.

Screenshot dell&#39;analisi dello stack con il nome di una funzione generata automaticamente.

Per risolvere questo problema, Chrome DevTools ora supporta la ridenominazione di queste funzioni tramite le mappe di origine. Se una mappa di origine include una voce del nome per l'inizio dell'ambito di una funzione (ovvero il cursore sinistro dell'elenco dei parametri), il frame della chiamata dovrebbe visualizzare quel nome nell'analisi dello stack.

Fotogrammi di chiamata semplici in Angular

La ridenominazione dei frame di chiamata in Angular è un impegno continuo. Ci aspettiamo che questi miglioramenti vengano apportati gradualmente nel tempo.

Durante l'analisi dei modelli HTML scritti dagli autori, il compilatore Angular genera un codice TypeScript che viene infine traslato nel codice JavaScript caricato ed eseguito dal browser.

Nell'ambito di questo processo di generazione del codice, vengono create anche le mappe di origine. Attualmente stiamo esplorando modi per includere i nomi delle funzioni nel campo "names" delle mappe di origine e fare riferimento a questi nomi nelle mappature tra il codice generato e il codice originale.

Ad esempio, se viene generata una funzione per un listener di eventi e il suo nome non è facile da usare o è stato rimosso durante la minimizzazione, le mappe di origine ora possono includere il nome più intuitivo per questa funzione nel campo "nomi" e la mappatura per l'inizio dell'ambito della funzione ora può fare riferimento a questo nome (ovvero la virgola sinistra dell'elenco dei parametri). Chrome DevTools utilizzerà questi nomi per rinominare i frame di chiamata nelle analisi dello stack.

Prospettive future

Utilizzare Angular come progetto pilota collaudato per verificare il nostro lavoro è stata un'esperienza eccezionale. Ci piacerebbe ricevere la risposta degli sviluppatori di framework e fornire un feedback su questi punti di estensione.

Ci sono altre aree che vorremmo esplorare. In particolare, come migliorare l'esperienza di profilazione in DevTools.