Aggiornamento dell'architettura DevTools: migrazione ai moduli JavaScript

Tim van der Lippe
Tim van der Lippe

Come forse saprai, Chrome DevTools è un'applicazione web scritta utilizzando HTML, CSS e JavaScript. Nel corso degli anni, DevTools è diventato più ricco di funzionalità, più intelligente e più esperto della piattaforma web più ampia. Sebbene DevTools sia cresciuto nel corso degli anni, la sua architettura è in gran parte simile a quella originale quando faceva ancora parte di WebKit.

Questo post fa parte di una serie di post del blog che descrivono le modifiche che stiamo apportando all'architettura di DevTools e alla sua modalità di creazione. Spiegheremo come ha funzionato storicamente DevTools, quali erano i vantaggi e le limitazioni e cosa abbiamo fatto per alleviarle. Pertanto, approfondiamo i sistemi di moduli, come caricare il codice e come abbiamo finito per utilizzare i moduli JavaScript.

All'inizio non c'era niente

Sebbene l'attuale panorama frontend offra una serie di sistemi di moduli con strumenti appositamente progettati, oltre al formato dei moduli JavaScript ora standardizzato, nessuno di questi esisteva al momento della creazione di DevTools. DevTools è basato su codice inizialmente rilasciato in WebKit più di 12 anni fa.

La prima menzione di un sistema di moduli in DevTools risale al 2012: l'introduzione di un elenco di moduli con un elenco associato di origini. Faceva parte dell'infrastruttura Python utilizzata all'epoca per compilare e creare DevTools. Una modifica successiva ha estratto tutti i moduli in un file frontend_modules.json separato (commit) nel 2013 e poi in file module.json separati (commit) nel 2014.

Un file module.json di esempio:

{
  "dependencies": [
    "common"
  ],
  "scripts": [
    "StylePane.js",
    "ElementsPanel.js"
  ]
}

Dal 2014, il pattern module.json viene utilizzato in DevTools per specificare i moduli e i file di origine. Nel frattempo, l'ecosistema web si è evoluto rapidamente e sono stati creati più formati di moduli, tra cui UMD, CommonJS e i moduli JavaScript eventualmente standardizzati. Tuttavia, DevTools ha mantenuto il formato module.json.

Sebbene DevTools continuasse a funzionare, l'utilizzo di un sistema di moduli non standardizzato e univoco presentava alcuni svantaggi:

  1. Il formato module.json richiedeva strumenti di compilazione personalizzati, simili ai moderni bundler.
  2. Non era presente l'integrazione con l'IDE, il che richiedeva strumenti personalizzati per generare file comprensibili dalle IDE moderne (lo script originale per generare file jsconfig.json per VS Code).
  3. Funzioni, classi e oggetti sono stati inseriti nell'ambito globale per consentire la condivisione tra i moduli.
  4. I file erano dipendenti dall'ordine, il che significa che l'ordine in cui erano elencati i file sources era importante. Non c'era alcuna garanzia che il codice di cui ti servivi sarebbe stato caricato, a parte il fatto che una persona lo aveva verificato.

Nel complesso, valutando lo stato attuale del sistema dei moduli in DevTools e negli altri formati dei moduli (più utilizzati), abbiamo concluso che il pattern module.json creava più problemi di quanti ne risolvesse ed era giunto il momento di pianificare il passaggio ad altri formati.

I vantaggi degli standard

Tra i sistemi di moduli esistenti, abbiamo scelto i moduli JavaScript come destinazione della migrazione. Al momento della decisione, i moduli JavaScript erano ancora disponibili dietro un flag in Node.js e una grande quantità di pacchetti disponibili su NPM non aveva un bundle di moduli JavaScript che potevamo utilizzare. Nonostante ciò, abbiamo concluso che i moduli JavaScript erano l'opzione migliore.

Il vantaggio principale dei moduli JavaScript è che si tratta del formato del modulo standardizzato per JavaScript. Quando abbiamo elencato i lati negativi del module.json (vedi sopra), ci siamo resi conto che quasi tutti erano legati all'utilizzo di un formato del modulo non standardizzato e univoco.

La scelta di un formato del modulo non standardizzato significa che dobbiamo investire tempo nella creazione di integrazioni con gli strumenti di build e gli strumenti utilizzati dai nostri manutentori.

Queste integrazioni erano spesso fragili e non supportavano le funzionalità, richiedevano tempi di manutenzione aggiuntivi e a volte causavano bug impercettibili che alla fine venivano inviati agli utenti.

Poiché i moduli JavaScript erano lo standard, gli IDE come VS Code, i tipi di controllo come Closure Compiler/TypeScript e gli strumenti di compilazione come Rollup/minifier erano in grado di comprendere il codice sorgente che abbiamo scritto. Inoltre, quando un nuovo manutentore si unisce al team di DevTools, non deve perdere tempo a imparare un formato module.json proprietario, mentre probabilmente conosce già i moduli JavaScript.

Ovviamente, quando DevTools è stato creato inizialmente, non esisteva nessuno dei vantaggi sopra elencati. Per arrivare a questo punto sono stati necessari anni di lavoro nei gruppi di standard, nelle implementazioni di runtime e negli sviluppatori che utilizzano i moduli JavaScript per fornire feedback. Tuttavia, quando sono diventati disponibili i moduli JavaScript, abbiamo dovuto scegliere: continuare a gestire il nostro formato o investire nella migrazione al nuovo.

Il costo del nuovo

Anche se i moduli JavaScript offrono molti vantaggi che vorremmo utilizzare, siamo rimasti nel mondo non standard di module.json. Per trarre vantaggio dai moduli JavaScript, abbiamo dovuto investire in modo significativo nella pulizia del debito tecnico, eseguendo una migrazione che potrebbe potenzialmente interrompere le funzionalità e introdurre bug di regressione.

A questo punto, non era una questione di "Vogliamo utilizzare i moduli JavaScript?", ma di "Quanto costa poter utilizzare i moduli JavaScript?". In questo caso, abbiamo dovuto valutare il rischio di causare problemi agli utenti con le regressioni, il costo degli ingegneri che hanno impiegato (molto) tempo per la migrazione e lo stato peggiore temporaneo in cui avremmo lavorato.

L'ultimo punto si è rivelato molto importante. Anche se in teoria potremmo utilizzare i moduli JavaScript, durante una migrazione finiremmo con un codice che dovrebbe tenere conto sia di module.json che dei moduli JavaScript. Non solo era tecnicamente difficile da realizzare, ma significava anche che tutti gli ingegneri che lavoravano su DevTools dovevano sapere come lavorare in questo ambiente. Dovrebbero chiedersi continuamente: "Per questa parte della base di codice, si tratta di moduli module.json o JavaScript e come faccio a apportare modifiche?".

Anteprima: il costo nascosto di aiutare i nostri colleghi a eseguire una migrazione è stato maggiore del previsto.

Dopo l'analisi dei costi, abbiamo concluso che valeva comunque la pena eseguire la migrazione ai moduli JavaScript. Pertanto, i nostri obiettivi principali erano i seguenti:

  1. Assicurati di sfruttare al meglio i vantaggi dell'utilizzo dei moduli JavaScript.
  2. Assicurati che l'integrazione con il sistema esistente basato su module.json sia sicura e non abbia un impatto negativo sugli utenti (bug di regressione, frustrazione degli utenti).
  3. Aiuta tutti i manutentori di DevTools a eseguire la migrazione, principalmente con controlli e bilanci integrati per evitare errori accidentali.

Fogli di lavoro, trasformazioni e debito tecnico

Sebbene lo scopo fosse chiaro, le limitazioni imposte dal formato module.json si sono rivelate difficili da aggirare. Ci sono volute diverse iterazioni, prototipi e modifiche all'architettura prima di sviluppare una soluzione soddisfacente. Abbiamo scritto un documento di progettazione con la strategia di migrazione che abbiamo scelto. La documentazione di progettazione elencava anche la nostra stima iniziale dei tempi: 2-4 settimane.

Spoiler alert: la parte più intensa della migrazione ha richiesto 4 mesi e l'intera operazione è durata 7 mesi.

Il piano iniziale, tuttavia, ha superato la prova del tempo: insegneremo al runtime di DevTools a caricare tutti i file elencati nell'array scripts nel file module.json utilizzando il vecchio metodo, mentre tutti i file elencati nell'array modules con l'importazione dinamica dei moduli JavaScript. Qualsiasi file che si trovi nell'array modules può utilizzare le importazioni/esportazioni di ES.

Inoltre, eseguiremo la migrazione in due fasi (l'ultima fase verrà suddivisa in due sottofasi, vedi di seguito): le fasi export e import. Lo stato del modulo in ogni fase veniva monitorato in un foglio di lavoro di grandi dimensioni:

Foglio di lavoro per la migrazione dei moduli JavaScript

Uno snippet del foglio di avanzamento è disponibile pubblicamente qui.

export-phase

La prima fase consiste nell'aggiungere istruzioni export per tutti i simboli che dovevano essere condivisi tra moduli/file. La trasformazione verrà automatizzata eseguendo uno script per cartella. Dato che nel mondo di module.json esiste il seguente simbolo:

Module.File1.exported = function() {
  console.log('exported');
  Module.File1.localFunctionInFile();
};
Module.File1.localFunctionInFile = function() {
  console.log('Local');
};

dove Module è il nome del modulo e File1 il nome del file. Nel nostro sourcetree, si tratta di front_end/module/file1.js.

Il risultato sarà il seguente:

export function exported() {
  console.log('exported');
  Module.File1.localFunctionInFile();
}
export function localFunctionInFile() {
  console.log('Local');
}

/** Legacy export object */
Module.File1 = {
  exported,
  localFunctionInFile,
};

Inizialmente, il nostro piano era di riscrivere anche le importazioni dello stesso file durante questa fase. Ad esempio, nell'esempio precedente riscrivi Module.File1.localFunctionInFile in localFunctionInFile. Tuttavia, ci siamo resi conto che sarebbe stato più facile automatizzare e applicare in modo più sicuro se avessimo separato queste due trasformazioni. Pertanto, l'operazione "esegui la migrazione di tutti i simboli nello stesso file" diventerà la seconda sottofase della fase import.

Poiché l'aggiunta della parola chiave export in un file trasforma il file da uno "script" in un "modulo", gran parte dell'infrastruttura di DevTools ha dovuto essere aggiornata di conseguenza. Sono inclusi il runtime (con importazione dinamica), ma anche strumenti come ESLint per l'esecuzione in modalità modulo.

Una scoperta che abbiamo fatto durante la risoluzione di questi problemi è che i nostri test venivano eseguiti in modalità "sloppy". Poiché i moduli JavaScript presuppongono che i file vengano eseguiti in modalità "use strict", ciò influirebbe anche sui nostri test. A quanto pare, un numero non trascurabile di test si basava su questa negligenza, incluso un test che utilizzava un'istruzione with 😱.

Alla fine, l'aggiornamento della prima cartella per includere le istruzioni export ha richiesto circa una settimana e diversi tentativi con reland.

import-phase

Dopo che tutti i simboli sono stati esportati utilizzando le istruzioni export e sono rimasti nell'ambito globale (legacy), abbiamo dovuto aggiornare tutti i riferimenti ai simboli tra file per utilizzare le importazioni ES. L'obiettivo finale è rimuovere tutti gli "oggetti di esportazione precedenti", ripulendo l'ambito globale. La trasformazione verrà automatizzata eseguendo uno script per cartella.

Ad esempio, per i seguenti simboli esistenti nel mondo module.json:

Module.File1.exported();
AnotherModule.AnotherFile.alsoExported();
SameModule.AnotherFile.moduleScoped();

Verranno trasformati in:

import * as Module from '../module/Module.js';
import * as AnotherModule from '../another_module/AnotherModule.js';

import {moduleScoped} from './AnotherFile.js';

Module.File1.exported();
AnotherModule.AnotherFile.alsoExported();
moduleScoped();

Tuttavia, questo approccio presentava alcuni inconvenienti:

  1. Non tutti i simboli sono stati denominati Module.File.symbolName. Alcuni simboli erano denominati solo Module.File o addirittura Module.CompletelyDifferentName. Questa incoerenza ha comportato la necessità di creare una mappatura interna dal vecchio oggetto globale al nuovo oggetto importato.
  2. A volte si verificano conflitti tra i nomi a livello di modulo. In particolare, abbiamo utilizzato un pattern per dichiarare determinati tipi di Events, in cui ogni simbolo era denominato semplicemente Events. Ciò significa che se ascoltavi più tipi di eventi dichiarati in file diversi, si verificava un conflitto di nomi nell'istruzione import per questi Events.
  3. A quanto pare, esistevano dipendenze circolari tra i file. Questo non era un problema in un contesto di ambito globale, poiché l'utilizzo del simbolo avveniva dopo il caricamento di tutto il codice. Tuttavia, se hai bisogno di un import, la dipendenza circolare verrà esplicitata. Questo non è un problema immediato, a meno che non siano presenti chiamate di funzioni con effetti collaterali nel codice a livello globale, come accadeva anche in DevTools. Tutto sommato, sono stati necessari alcuni interventi e il refactoring per rendere sicura la trasformazione.

Un nuovo mondo con i moduli JavaScript

A febbraio 2020, 6 mesi dopo l'inizio a settembre 2019, sono state eseguite le ultime operazioni di pulizia nella cartella ui/. Questo ha segnato la fine non ufficiale della migrazione. Dopo aver lasciato che le cose si sistemassero, abbiamo contrassegnato ufficialmente la migrazione come completata il 5 marzo 2020. 🎉

Ora tutti i moduli di DevTools utilizzano i moduli JavaScript per condividere il codice. Inseriamo ancora alcuni simboli nell'ambito globale (nei file module-legacy.js) per i nostri test precedenti o per l'integrazione con altre parti dell'architettura di DevTools. Verranno rimosse nel tempo, ma non le consideriamo un blocco per lo sviluppo futuro. Abbiamo anche una guida di stile per l'utilizzo dei moduli JavaScript.

Statistiche

Le stime conservative per il numero di elenchi di modifiche (abbreviazione di elenchi di modifiche, il termine utilizzato in Gerrit per rappresentare una modifica, simile a una richiesta pull di GitHub) coinvolti in questa migrazione sono di circa 250 elenchi di modifiche, eseguiti in gran parte da due ingegneri. Non disponiamo di statistiche definitive sulle dimensioni delle modifiche apportate, ma una stima prudente delle righe modificate (calcolata come somma della differenza assoluta tra inserzioni ed eliminazioni per ogni CL) è di circa 30.000 righe (~20% di tutto il codice frontend di DevTools).

Il primo file che utilizza export è stato rilasciato in Chrome 79, reso disponibile nella versione stabile a dicembre 2019. L'ultima modifica per eseguire la migrazione a import è stata rilasciata in Chrome 83, reso disponibile nella versione stabile a maggio 2020.

Siamo a conoscenza di una regressione che è stata inviata a Chrome stabile e che è stata introdotta nell'ambito di questa migrazione. Il completamento automatico degli snippet nel menu dei comandi si è interrotto a causa di un'esportazione default estranea. Abbiamo riscontrato diverse altre regressioni, ma le nostre suite di test automatici e gli utenti di Chrome Canary le hanno segnalate e le abbiamo corrette prima che potessero raggiungere gli utenti di Chrome versione stabile.

Puoi vedere il percorso completo (non tutti i CL sono associati a questo bug, ma la maggior parte lo è) registrato su crbug.com/1006759.

Che cosa abbiamo imparato

  1. Le decisioni prese in passato possono avere un impatto duraturo sul tuo progetto. Anche se i moduli JavaScript (e altri formati di moduli) erano disponibili da un po' di tempo, DevTools non era in grado di giustificare la migrazione. Decidere quando eseguire la migrazione e quando no è difficile e si basa su supposizioni.
  2. Le nostre stime iniziali erano in settimane anziché in mesi. Ciò è dovuto in gran parte al fatto che abbiamo riscontrato più problemi inaspettati del previsto nella nostra analisi dei costi iniziale. Anche se il piano di migrazione era solido, il debito tecnico era (più spesso di quanto avremmo voluto) un blocco.
  3. La migrazione dei moduli JavaScript ha incluso una grande quantità di operazioni di pulizia del debito tecnico (apparentemente non correlate). La migrazione a un formato di modulo standardizzato moderno ci ha permesso di allineare le nostre best practice di programmazione allo sviluppo web moderno. Ad esempio, siamo riusciti a sostituire il nostro bundler Python personalizzato con una configurazione di aggregazione minima.
  4. Nonostante l'impatto significativo sulla nostra base di codice (~20% di codice modificato), sono state registrate pochissime regressioni. Abbiamo riscontrato numerosi problemi durante la migrazione dei primi due file, ma dopo un po' di tempo abbiamo ottenuto un flusso di lavoro solido e parzialmente automatizzato. Ciò ha comportato un impatto negativo minimo per gli utenti stabili durante questa migrazione.
  5. Insegnare le complessità di una determinata migrazione ad altri manutentori è difficile e a volte impossibile. Le migrazioni di questa portata sono difficili da seguire e richiedono una conoscenza approfondita del dominio. Il trasferimento di queste conoscenze di dominio ad altri che lavorano nella stessa base di codice non è auspicabile per il lavoro che stanno svolgendo. Sapere cosa condividere e quali dettagli non condividere è un'arte, ma necessaria. È quindi fondamentale ridurre il numero di migrazioni di grandi dimensioni o, almeno, non eseguirle contemporaneamente.

Scaricare i canali di anteprima

Valuta la possibilità di utilizzare Chrome Canary, Dev o Beta come browser di sviluppo predefinito. Questi canali di anteprima ti consentono di accedere alle funzionalità più recenti di DevTools, di testare API di piattaforme web all'avanguardia e di trovare i problemi sul tuo sito prima che lo facciano i tuoi utenti.

Contatta il team di Chrome DevTools

Utilizza le seguenti opzioni per discutere di nuove funzionalità, aggiornamenti o qualsiasi altro argomento relativo a DevTools.