Aggiornamento dell'architettura DevTools: migrazione ai moduli JavaScript

Tim van der Lippe
Tim van der Lippe

Come forse già saprai, Chrome DevTools è un'applicazione web scritta utilizzando HTML, CSS e JavaScript. Nel corso degli anni, DevTools è diventato più ricco di funzionalità, intelligente e competente sulla piattaforma web più ampia. Anche se DevTools si è ampliato nel corso degli anni, la sua architettura ricorda in gran parte l'architettura 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 il modo in cui viene creata. Spiegheremo come DevTools ha funzionato storicamente, quali sono i vantaggi e i limiti e cosa abbiamo fatto per attenuare questi limiti. Pertanto, approfondiamo i sistemi di moduli, come caricare il codice e come finiamo con l'utilizzo dei moduli JavaScript.

All'inizio, non c'era

Sebbene l'attuale panorama di frontend abbia una varietà di sistemi di moduli con strumenti basati su questi ultimi, nonché il formato dei moduli JavaScript standardizzati, nessuno di questi esisteva quando è stato creato per la prima volta DevTools. DevTools si basa sul codice fornito inizialmente in WebKit più di 12 anni fa.

Il primo riferimento a un sistema di moduli in DevTools risale al 2012: l'introduzione di un elenco di moduli con un elenco di origini associato. Questo faceva parte dell'infrastruttura Python utilizzata a quel tempo per compilare e creare DevTools. Una modifica di follow-up 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.

Esempio di file module.json:

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

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

Anche se DevTools non funzionava, c'erano un paio di svantaggi legati all'utilizzo di un sistema di moduli univoco e non standardizzato:

  1. Il formato module.json richiedeva strumenti di creazione personalizzati, in modo simile ai bundleer moderni.
  2. Non era prevista l'integrazione con IDE, il che richiedeva strumenti personalizzati per generare file che gli IDE moderni erano comprensibili (lo script originale per generare file jsconfig.json per VS Code).
  3. Funzioni, classi e oggetti sono stati inseriti nell'ambito globale per rendere possibile la condivisione tra i moduli.
  4. I file erano dipendenti dall'ordine, il che significa che l'ordine in cui sources erano elencati era importante. Non c'era alcuna garanzia che il codice su cui fai affidamento sarebbe stato caricato, a parte il fatto che un essere umano lo aveva verificato.

Nel complesso, durante la valutazione dello stato attuale del sistema di moduli in DevTools e negli altri formati di moduli (più comunemente utilizzati), abbiamo concluso che il pattern module.json stava creando più problemi di quanti non ne avesse risolto ed era giunto il momento di abbandonare la configurazione.

I vantaggi degli standard

Tra i sistemi di moduli esistenti, abbiamo scelto i moduli JavaScript come quelli in cui eseguire la migrazione. Al momento della decisione, i moduli JavaScript venivano ancora spediti dietro un flag in Node.js e una grande quantità di pacchetti disponibili in Gestione dei partner di rete non disponeva di un bundle di moduli JavaScript da poter utilizzare. Nonostante ciò, abbiamo concluso che i moduli JavaScript erano l'opzione migliore.

Il vantaggio principale dei moduli JavaScript è che sono il formato standardizzato dei moduli per JavaScript. Quando abbiamo elencato gli svantaggi di module.json (vedi sopra), ci siamo resi conto che quasi tutti erano correlati all'utilizzo di un formato di modulo univoco e non standardizzato.

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

Queste integrazioni spesso erano fragili e mancavano il supporto per le funzionalità, che richiedevano tempi di manutenzione aggiuntivi e a volte causavano lievi bug che alla fine venivano forniti agli utenti.

Poiché i moduli JavaScript erano lo standard, significava che IDE come VS Code, strumenti di controllo dei caratteri come Closure Compiler/TypeScript e strumenti di creazione come Rollup/minifiers sarebbero stati in grado di comprendere il codice sorgente che abbiamo scritto. Inoltre, quando un nuovo gestore si unisce al team DevTools, non deve perdere tempo ad apprendere un formato module.json proprietario, mentre è probabile che abbia già familiarità con i moduli JavaScript.

Naturalmente, quando DevTools è stato creato inizialmente, non esisteva nessuno dei vantaggi elencati sopra. Ci sono voluti anni di lavoro per quanto riguarda gruppi di standard, implementazioni runtime e sviluppatori che usavano moduli JavaScript che fornivano feedback per arrivare al punto in cui si trovano ora. Ma quando sono diventati disponibili i moduli JavaScript, abbiamo dovuto scegliere se continuare a mantenere il nostro formato o investire nella migrazione a quello nuovo.

Il costo del nuovo

Anche se i moduli JavaScript presentavano molti vantaggi che vorremmo utilizzare, siamo rimasti nel mondo non standard di module.json. I vantaggi dei moduli JavaScript ci hanno costretto a investire in modo significativo nella risanamento 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 "Vuoi utilizzare i moduli JavaScript?", ma una domanda di "Quanto è costoso poter utilizzare i moduli JavaScript?". Qui, dovevamo bilanciare il rischio di rompere i nostri utenti con le regressioni, il costo dei tecnici che impiegavano (molto) tempo per la migrazione e lo stato temporaneo peggiore in cui avremmo lavorato.

Quest'ultimo punto si è rivelato molto importante. Anche se in teoria potremmo arrivare ai moduli JavaScript, durante una migrazione ci trovavamo del codice che avrebbe dovuto prendere in considerazione entrambi i moduli module.json e JavaScript. Non solo era tecnicamente difficile da ottenere, ma significava anche che tutti gli ingegneri che lavoravano su DevTools avrebbero dovuto sapere come lavorare in questo ambiente. Dovrebbero chiedersi continuamente "Per questa parte del codebase, si tratta di moduli module.json o JavaScript e come faccio ad apportare modifiche?".

Anteprima: il costo nascosto di guidare i nostri assistenti nella migrazione attraverso una migrazione è stato maggiore del previsto.

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

  1. Assicurarsi che l'utilizzo dei moduli JavaScript garantisca i massimi benefici.
  2. Assicurati che l'integrazione con il sistema basato su module.json esistente sia sicura e non comporti un impatto negativo sugli utenti (bug di regressione, frustrazione degli utenti).
  3. Guida tutti i gestori DevTools nella migrazione, principalmente con controlli e saldi integrati per evitare errori accidentali.

Fogli di lavoro, trasformazioni e debito tecnico

Sebbene l'obiettivo fosse chiaro, le limitazioni imposte dal formato module.json si sono rivelate difficili da aggirare. Sono state necessarie diverse iterazioni, prototipi e modifiche all'architettura prima di sviluppare una soluzione con cui eravamo a proprio agio. Abbiamo scritto un documento di progettazione per la strategia di migrazione alla fine. Il documento di progettazione riportava anche la nostra stima iniziale del tempo: 2-4 settimane.

Spoiler: la parte più intensa della migrazione ha richiesto 4 mesi e dall'inizio alla fine 7 mesi.

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

Inoltre, eseguiremo la migrazione in due fasi (alla fine suddivideremo l'ultima fase in due sottofasi, vedi di seguito): la fase export e import. Lo stato di quale modulo si trova in quale fase è stato 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 fase

La prima fase prevedeva l'aggiunta di istruzioni export per tutti i simboli che avrebbero dovuto essere condivisi tra moduli/file. La trasformazione sarebbe automatizzata eseguendo uno script per cartella. Dato il seguente simbolo esiste nel mondo module.json:

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

Qui, Module è il nome del modulo e File1 il nome del file. Nella struttura ad albero di origine, corrisponde a front_end/module/file1.js.

La conversione viene trasformata nel seguente modo:

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 prevedeva di riscrivere anche le importazioni degli stessi file durante questa fase. Ad esempio, nell'esempio precedente riscriveremmo Module.File1.localFunctionInFile in localFunctionInFile. Tuttavia, ci siamo resi conto che l'automazione sarebbe stata più facile e sicura se avessimo separato le due trasformazioni. Di conseguenza, 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 "script" a un "modulo", gran parte dell'infrastruttura DevTools ha dovuto essere aggiornata di conseguenza. Ciò includeva il runtime (con importazione dinamica), ma anche strumenti come ESLint da eseguire in modalità modulo.

Una scoperta che abbiamo scoperto durante la risoluzione di questi problemi è che i nostri test erano eseguiti in modalità "discontinua". Poiché i moduli JavaScript implicano che i file vengono eseguiti in modalità "use strict", ciò influirebbe anche sui nostri test. Come risultato, una quantità non banale di test si basava su questa perdita, tra cui un test che utilizzava un'istruzione with XYZ.

Alla fine, l'aggiornamento della prima cartella in modo da includere le istruzioni export ha richiesto circa una settimana e più tentativi con la riassegnazione.

import fase

Dopo che tutti i simboli sono stati entrambi 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 sarebbe la rimozione di tutti gli "oggetti di esportazione legacy", ripulindo l'ambito globale. La trasformazione sarebbe automatizzata eseguendo uno script per cartella.

Ad esempio, per i seguenti simboli che esistono 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, ci sono stati alcuni elementi all'interno di questo approccio:

  1. Non tutti i simboli hanno il nome Module.File.symbolName. Alcuni simboli avevano il nome esclusivo di Module.File o persino Module.CompletelyDifferentName. Questa incoerenza significava dover creare un mapping interno dal vecchio oggetto globale al nuovo oggetto importato.
  2. A volte si verificano conflitti tra i nomi moduleScoped. In particolare, abbiamo utilizzato un pattern per dichiarare alcuni tipi di Events, in cui ogni simbolo veniva chiamato semplicemente Events. Ciò significa che se stai ascoltando più tipi di eventi dichiarati in file diversi, si verifica un conflitto di nomi nell'istruzione import per quei Events.
  3. Come risultato, c'erano dipendenze circolari tra i file. Questo non andava bene 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 diventerà esplicita. Questo non è un problema immediato, a meno che tu non abbia chiamate a funzioni collaterali nel codice dell'ambito globale, di cui era disponibile anche DevTools. Nel complesso, sono stati necessari alcuni interventi chirurgici e un refactoring per rendere sicura la trasformazione.

Un mondo completamente nuovo con i moduli JavaScript

A febbraio 2020, 6 mesi dopo l'inizio di settembre 2019, le ultime operazioni di pulizia sono state eseguite nella cartella ui/. Questa operazione ha segnato la fine non ufficiale della migrazione. Dopo aver fatto decollare la polvere, abbiamo contrassegnato ufficialmente la migrazione come terminata il 5 marzo 2020. 🎉

Ora tutti i moduli in DevTools usano i moduli JavaScript per condividere il codice. Mettiamo comunque alcuni simboli nell'ambito globale (nei file module-legacy.js) per i nostri test legacy o per l'integrazione con altre parti dell'architettura DevTools. Col tempo verranno rimosse, ma non le consideriamo un ostacolo per lo sviluppo futuro. È disponibile anche una guida di stile per l'utilizzo dei moduli JavaScript.

Statistiche

Le stime conservatrici per il numero di CL (abbreviazione di elenco modifiche, il termine utilizzato in Gerrit che rappresenta una modifica, simile a una richiesta di pull GitHub) coinvolti in questa migrazione sono di circa 250 CL, eseguite in gran parte da 2 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 gli ordini e le eliminazioni per ogni CL) è di circa 30.000 (circa il 20% di tutto il codice frontend di DevTools).

Il primo file che utilizza export è stato spedito in Chrome 79 e rilasciato nella versione stabile a dicembre 2019. L'ultima modifica apportata alla migrazione a import è stata fornita in Chrome 83 e rilasciato nella versione stabile a maggio 2020.

Siamo a conoscenza di una regressione inviata alla versione stabile di Chrome e che è stata introdotta nell'ambito di questa migrazione. Il completamento automatico degli snippet nel menu dei comandi non è disponibile a causa di un'esportazione default estranea. Sono state apportate diverse altre regressioni, ma le nostre suite di test automatiche e gli utenti di Chrome Canary le hanno segnalate e le abbiamo risolte prima che riuscissero a raggiungere gli utenti stabili di Chrome.

Puoi vedere l'intero percorso (non tutti i CL sono allegati a questo bug, ma la maggior parte di essi) è registrato su crbug.com/1006759.

Che cosa abbiamo imparato

  1. Le decisioni prese in passato possono avere un impatto a lungo termine 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 e quando non eseguire la migrazione è difficile e si basa su ipotesi ragionate.
  2. Inizialmente, la stima del tempo era espressa in settimane anziché in mesi. Ciò deriva in gran parte dal fatto che abbiamo trovato più problemi inaspettati di quanto previsto nella nostra analisi iniziale dei costi. Anche se il piano di migrazione era solido, il blocco tecnico è stato (più spesso di quanto avremmo voluto) il debito tecnico.
  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 moderno formato di moduli standardizzato ci ha permesso di riallineare le nostre best practice di programmazione con lo sviluppo web moderno. Ad esempio, siamo riusciti a sostituire il nostro bundler Python personalizzato con una configurazione di aggregazione minima.
  4. Nonostante il grande impatto sul nostro codebase (circa il 20% del codice è cambiato), sono state riportate pochissime regressioni. Anche se abbiamo avuto diversi problemi nella migrazione dei primi due file, dopo un po' di tempo abbiamo avuto un flusso di lavoro solido e parzialmente automatizzato. Di conseguenza, l'impatto negativo sui nostri utenti stabili è stato minimo per questa migrazione.
  5. Insegnare le complessità di una particolare migrazione ad altri assistenti è difficile e a volte impossibile. Le migrazioni di questa scala sono difficili da seguire e richiedono molta conoscenza del dominio. Il trasferimento di questa conoscenza del dominio ad altre persone che lavorano sullo stesso codebase non è di per sé auspicabile per il lavoro che stanno svolgendo. Sapere cosa condividere e quali dettagli non condividere è un'arte, ma necessario. È quindi fondamentale ridurre la quantità di migrazioni di grandi dimensioni o quanto meno non eseguirle contemporaneamente.

Scarica i canali in anteprima

Prendi in considerazione l'utilizzo di Chrome Canary, Dev o beta come browser di sviluppo predefinito. Questi canali in anteprima ti consentono di accedere alle funzionalità di DevTools più recenti, di testare le API per piattaforme web all'avanguardia e di individuare eventuali problemi sul tuo sito prima che lo facciano gli utenti.

Contattare il team di Chrome DevTools

Utilizza le opzioni seguenti per discutere delle nuove funzionalità e delle modifiche nel post o di qualsiasi altra cosa relativa a DevTools.

  • Inviaci un suggerimento o un feedback tramite crbug.com.
  • Segnala un problema DevTools utilizzando Altre opzioni   Altre   > Guida > Segnala i problemi di DevTools in DevTools.
  • Tweet all'indirizzo @ChromeDevTools.
  • Lascia commenti sui video di YouTube o sui suggerimenti di DevTools in DevTools Video di YouTube.