Vernieuwing van de DevTools-architectuur: migreren naar JavaScript-modules

Tim van der Lippe
Tim van der Lippe

Zoals u wellicht weet, is Chrome DevTools een webapplicatie geschreven met behulp van HTML, CSS en JavaScript. In de loop der jaren is DevTools rijker aan functies, slimmer en beter geïnformeerd geworden over het bredere webplatform. Hoewel DevTools in de loop der jaren is uitgebreid, lijkt de architectuur grotendeels op de oorspronkelijke architectuur toen het nog deel uitmaakte van WebKit .

Dit bericht maakt deel uit van een reeks blogposts waarin de wijzigingen worden beschreven die we aanbrengen in de architectuur van DevTools en hoe deze is gebouwd . We zullen uitleggen hoe DevTools historisch heeft gewerkt, wat de voordelen en beperkingen waren en wat we hebben gedaan om deze beperkingen te verlichten. Laten we daarom eens dieper ingaan op modulesystemen, hoe code moet worden geladen en hoe we uiteindelijk JavaScript-modules hebben gebruikt.

In het begin was er niets

Hoewel het huidige frontend-landschap een verscheidenheid aan modulesystemen kent met daaromheen gebouwde tools, evenals het nu gestandaardiseerde JavaScript-moduleformaat , bestond geen van deze toen DevTools voor het eerst werd gebouwd. DevTools is gebouwd bovenop code die meer dan twaalf jaar geleden oorspronkelijk in WebKit werd geleverd.

De eerste vermelding van een modulesysteem in DevTools stamt uit 2012: de introductie van een lijst met modules met een bijbehorende bronnenlijst . Dit maakte deel uit van de Python-infrastructuur die destijds werd gebruikt om DevTools te compileren en te bouwen. Door een vervolgwijziging werden alle modules in 2013 geëxtraheerd naar een afzonderlijk frontend_modules.json bestand ( commit ) en vervolgens in afzonderlijke module.json bestanden ( commit ) in 2014.

Een voorbeeld van module.json -bestand:

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

Sinds 2014 wordt het patroon module.json in DevTools gebruikt om de modules en bronbestanden ervan te specificeren. Ondertussen ontwikkelde het web-ecosysteem zich snel en werden er meerdere moduleformaten gecreëerd, waaronder UMD, CommonJS en de uiteindelijk gestandaardiseerde JavaScript-modules. DevTools bleef echter bij het module.json -formaat.

Hoewel DevTools bleef werken, waren er een aantal nadelen aan het gebruik van een niet-gestandaardiseerd en uniek modulesysteem:

  1. Het module.json -formaat vereiste op maat gemaakte tools, vergelijkbaar met moderne bundelaars.
  2. Er was geen IDE-integratie, waarvoor aangepaste tools nodig waren om bestanden te genereren die moderne IDE's konden begrijpen ( het originele script om jsconfig.json-bestanden voor VS Code te genereren ).
  3. Functies, klassen en objecten zijn allemaal op de globale scope geplaatst om het delen tussen modules mogelijk te maken.
  4. Bestanden waren volgordeafhankelijk, wat betekent dat de volgorde waarin sources werden vermeld belangrijk was. Er was geen garantie dat de code waarop u vertrouwt, zou worden geladen, behalve dat een mens deze had geverifieerd.

Al met al kwamen we bij het evalueren van de huidige status van het modulesysteem in DevTools en de andere (meer algemeen gebruikte) moduleformaten tot de conclusie dat het module.json patroon meer problemen veroorzaakte dan oploste en dat het tijd was om onze verhuizing te plannen. ervan.

De voordelen van standaarden

Uit de bestaande modulesystemen hebben we JavaScript-modules gekozen om naartoe te migreren. Op het moment van dat besluit werden JavaScript-modules nog steeds achter een vlag in Node.js verzonden en een groot aantal pakketten beschikbaar op NPM beschikte niet over een JavaScript-modulebundel die we konden gebruiken. Desondanks kwamen we tot de conclusie dat JavaScript-modules de beste optie waren.

Het belangrijkste voordeel van JavaScript-modules is dat het het gestandaardiseerde moduleformaat voor JavaScript is. Toen we de nadelen van module.json opsomden (zie hierboven), realiseerden we ons dat ze bijna allemaal verband hielden met het gebruik van een niet-gestandaardiseerd en uniek moduleformaat.

Het kiezen van een moduleformaat dat niet-gestandaardiseerd is, betekent dat we zelf tijd moeten investeren in het bouwen van integraties met de bouwtools en tools die onze beheerders gebruikten.

Deze integraties waren vaak broos en ontbeerden ondersteuning voor functies, waardoor extra onderhoudstijd nodig was, wat soms leidde tot subtiele bugs die uiteindelijk naar gebruikers zouden worden verzonden.

Omdat JavaScript-modules de standaard waren, betekende dit dat IDE's zoals VS Code, typecheckers zoals Closure Compiler/TypeScript en buildtools zoals Rollup/minifiers de broncode die we schreven zouden kunnen begrijpen. Bovendien zou een nieuwe beheerder, wanneer hij zich bij het DevTools-team zou voegen, geen tijd hoeven te besteden aan het leren van een eigen module.json -formaat, terwijl hij (waarschijnlijk) al bekend zou zijn met JavaScript-modules.

Toen DevTools aanvankelijk werd gebouwd, bestonden uiteraard geen van de bovenstaande voordelen. Het kostte jaren van werk in standaardgroepen, runtime-implementaties en ontwikkelaars die JavaScript-modules gebruikten en feedback gaven om op het punt te komen waar ze nu zijn. Maar toen JavaScript-modules beschikbaar kwamen, moesten we een keuze maken: óf ons eigen formaat blijven behouden, óf investeren in de migratie naar het nieuwe.

De kosten van het glanzende nieuwe

Hoewel JavaScript-modules veel voordelen hadden die we graag zouden willen gebruiken, bleven we in de niet-standaard module.json wereld. Het benutten van de voordelen van JavaScript-modules betekende dat we aanzienlijk moesten investeren in het opruimen van technische schulden, het uitvoeren van een migratie die mogelijk functies kapot zou kunnen maken en regressie-bugs zou kunnen introduceren.

Op dit moment was het geen kwestie van "Willen we JavaScript-modules gebruiken?", maar een vraag van "Hoe duur is het om JavaScript-modules te kunnen gebruiken?" . Hier moesten we het risico van het breken van onze gebruikers afwegen tegen regressies, de kosten van technici die (een grote hoeveelheid) tijd besteden aan migreren en de tijdelijk slechtere toestand waarin we zouden werken.

Dat laatste punt bleek heel belangrijk. Hoewel we in theorie JavaScript-modules zouden kunnen bereiken, zouden we tijdens een migratie eindigen met code die rekening zou moeten houden met zowel module.json als JavaScript-modules. Dit was niet alleen technisch moeilijk te realiseren, het betekende ook dat alle engineers die aan DevTools werkten, moesten weten hoe ze in deze omgeving moesten werken. Ze zouden zich voortdurend moeten afvragen: "Is dit voor dit deel van de codebase module.json of JavaScript-modules en hoe kan ik wijzigingen aanbrengen?".

Sneak peek: de verborgen kosten voor het begeleiden van onze collega-onderhouders door een migratie waren groter dan we hadden verwacht.

Na de kostenanalyse kwamen we tot de conclusie dat het nog steeds de moeite waard was om naar JavaScript-modules te migreren. Daarom waren onze belangrijkste doelstellingen de volgende:

  1. Zorg ervoor dat het gebruik van JavaScript-modules zoveel mogelijk voordelen oplevert.
  2. Zorg ervoor dat de integratie met het bestaande module.json gebaseerde systeem veilig is en niet leidt tot negatieve gevolgen voor de gebruiker (regressiebugs, gebruikersfrustratie).
  3. Leid alle DevTools-onderhouders door de migratie, voornamelijk met ingebouwde checks and balances om onbedoelde fouten te voorkomen.

Spreadsheets, transformaties en technische schulden

Hoewel het doel duidelijk was, bleken de beperkingen die het module.json -formaat oplegde lastig te omzeilen. Er waren verschillende iteraties, prototypes en architectonische veranderingen nodig voordat we een oplossing ontwikkelden waar we ons prettig bij voelden. We schreven een ontwerpdocument met de migratiestrategie die we uiteindelijk hebben bereikt. Het ontwerpdocument vermeldde ook onze initiële tijdsschatting: 2-4 weken.

Spoiler alert: het meest intensieve deel van de migratie duurde 4 maanden en van begin tot eind 7 maanden!

Het oorspronkelijke plan doorstond echter de tand des tijds: we zouden de DevTools-runtime leren om alle bestanden in de scripts array in het module.json bestand op de oude manier te laden, terwijl alle bestanden in de modules -array met JavaScript-modules dynamische import . Elk bestand dat zich in de modules zou bevinden, zou ES-import/export kunnen gebruiken.

Daarnaast zouden we de migratie in 2 fasen uitvoeren (de laatste fase hebben we uiteindelijk opgesplitst in 2 deelfasen, zie hieronder): de export en import . De status van welke module zich in welke fase bevond, werd bijgehouden in een grote spreadsheet:

Spreadsheet voor migratie van JavaScript-modules

Een fragment van het voortgangsoverzicht is hier openbaar beschikbaar.

export -fase

De eerste fase zou bestaan ​​uit het toevoegen van export -statements voor alle symbolen die zouden moeten worden gedeeld tussen modules/bestanden. De transformatie zou geautomatiseerd worden, door per map een script uit te voeren . Gegeven dat het volgende symbool zou bestaan ​​in de module.json wereld:

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

(Hier is Module de naam van de module en File1 de naam van het bestand. In onze sourcetree zou dat front_end/module/file1.js zijn.)

Dit zou worden omgezet in het volgende:

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

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

Aanvankelijk was het ons plan om tijdens deze fase ook de import van hetzelfde bestand te herschrijven. In het bovenstaande voorbeeld zouden we bijvoorbeeld Module.File1.localFunctionInFile herschrijven naar localFunctionInFile . We realiseerden ons echter dat het gemakkelijker zou zijn om te automatiseren en veiliger toe te passen als we deze twee transformaties zouden scheiden. Daarom zou het "migreren van alle symbolen in hetzelfde bestand" de tweede subfase van de import worden.

Omdat het toevoegen van het trefwoord export aan een bestand het bestand transformeert van een "script" naar een "module", moest een groot deel van de DevTools-infrastructuur dienovereenkomstig worden bijgewerkt. Dit omvatte de runtime (met dynamische import), maar ook tools zoals ESLint om in modulemodus te draaien.

Eén ontdekking die we hebben gedaan tijdens het oplossen van deze problemen, is dat onze tests in "slordige" modus werden uitgevoerd. Omdat JavaScript-modules impliceren dat bestanden in de modus "use strict" worden uitgevoerd, zou dit ook onze tests beïnvloeden. Het bleek dat een niet-triviaal aantal tests op deze slordigheid vertrouwden, waaronder een test die een with -statement gebruikte 😱.

Uiteindelijk duurde het updaten van de allereerste map met export -statements ongeveer een week en meerdere pogingen met relands .

import

Nadat alle symbolen beide waren geëxporteerd met behulp van export -statements en binnen het globale bereik bleven (verouderd), moesten we alle verwijzingen naar symbolen tussen bestanden bijwerken om ES-importen te gebruiken. Het einddoel zou zijn om alle ‘legacy exportobjecten’ te verwijderen, waardoor de mondiale reikwijdte wordt opgeschoond. De transformatie zou geautomatiseerd worden, door per map een script uit te voeren .

Bijvoorbeeld voor de volgende symbolen die voorkomen in de module.json wereld:

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

Ze zouden worden omgezet 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();

Er waren echter enkele kanttekeningen bij deze aanpak:

  1. Niet elk symbool kreeg de naam Module.File.symbolName . Sommige symbolen heetten uitsluitend Module.File of zelfs Module.CompletelyDifferentName . Deze inconsistentie betekende dat we een interne mapping moesten maken van het oude globale object naar het nieuwe geïmporteerde object.
  2. Soms waren er botsingen tussen moduleScoped-namen. Het meest opvallend was dat we een patroon gebruikten voor het declareren van bepaalde soorten Events , waarbij elk symbool alleen de naam Events kreeg. Dit betekende dat als u luisterde naar meerdere typen gebeurtenissen die in verschillende bestanden waren gedeclareerd, er een naamconflict zou optreden in de import -statement voor die Events .
  3. Het bleek dat er sprake was van circulaire afhankelijkheden tussen bestanden. Dit was prima in een mondiale context, omdat het gebruik van het symbool plaatsvond nadat alle code was geladen. Als u echter een import nodig heeft, wordt de circulaire afhankelijkheid expliciet gemaakt. Dit is niet meteen een probleem, tenzij je neveneffect-functieaanroepen hebt in je globale scopecode, die DevTools ook had. Al met al was er enige operatie en refactoring nodig om de transformatie veilig te maken.

Een hele nieuwe wereld met JavaScript-modules

In februari 2020, 6 maanden na de start in september 2019, zijn de laatste opruimingen uitgevoerd in de ui/ map. Dit betekende het onofficiële einde van de migratie. Nadat het stof was neergedaald, hebben we de migratie op 5 maart 2020 officieel als voltooid gemarkeerd. 🎉

Nu gebruiken alle modules in DevTools JavaScript-modules om code te delen. We hebben nog steeds enkele symbolen op de globale scope geplaatst (in de module-legacy.js -bestanden) voor onze oudere tests of om te integreren met andere delen van de DevTools-architectuur. Deze zullen in de loop van de tijd worden verwijderd, maar we beschouwen ze niet als een belemmering voor toekomstige ontwikkeling. We hebben ook een stijlgids voor ons gebruik van JavaScript-modules .

Statistieken

Conservatieve schattingen voor het aantal CL's (afkorting voor changelist - de term die in Gerrit wordt gebruikt en die een verandering vertegenwoordigt - vergelijkbaar met een GitHub pull-request) die bij deze migratie betrokken is, bedragen ongeveer 250 CL's, grotendeels uitgevoerd door 2 engineers . We hebben geen definitieve statistieken over de omvang van de aangebrachte wijzigingen, maar een conservatieve schatting van het aantal gewijzigde regels (berekend als de som van het absolute verschil tussen invoegingen en verwijderingen voor elke CL) bedraagt ​​ongeveer 30.000 (~20% van alle frontend-code van DevTools). ) .

Het eerste bestand dat export gebruikt, is verzonden in Chrome 79 en is in december 2019 vrijgegeven voor stabiel. De laatste wijziging om te migreren naar import is verzonden in Chrome 83 en is in mei 2020 vrijgegeven voor stabiel.

We zijn op de hoogte van één regressie die naar Chrome stable is verzonden en die werd geïntroduceerd als onderdeel van deze migratie. Het automatisch aanvullen van fragmenten in het opdrachtmenu is mislukt vanwege een externe default . We hebben verschillende andere regressies gehad, maar onze geautomatiseerde testsuites en Chrome Canary-gebruikers hebben deze gerapporteerd en we hebben deze verholpen voordat ze stabiele Chrome-gebruikers konden bereiken.

Je kunt de volledige reis zien (niet alle CL's zijn aan deze bug gekoppeld, maar de meeste wel) ingelogd op crbug.com/1006759 .

Wat we hebben geleerd

  1. Beslissingen uit het verleden kunnen een langdurige impact hebben op uw project. Hoewel JavaScript-modules (en andere moduleformaten) al geruime tijd beschikbaar waren, kon DevTools de migratie niet rechtvaardigen. Beslissen wanneer wel en wanneer niet te migreren is moeilijk en gebaseerd op weloverwogen gissingen.
  2. Onze aanvankelijke tijdsschattingen waren in weken in plaats van in maanden. Dit komt grotendeels voort uit het feit dat we meer onverwachte problemen hebben aangetroffen dan we hadden verwacht in onze initiële kostenanalyse. Hoewel het migratieplan solide was, waren de technische schulden (vaker dan we hadden gewild) de blokkeerder.
  3. De migratie van JavaScript-modules omvatte een groot aantal (schijnbaar niet-gerelateerde) technische schuldensaneringen. Dankzij de migratie naar een modern gestandaardiseerd moduleformaat konden we onze best practices op het gebied van coderen afstemmen op de moderne webontwikkeling. We hebben bijvoorbeeld onze aangepaste Python-bundelr kunnen vervangen door een minimale Rollup-configuratie.
  4. Ondanks de grote impact op onze codebase (~20% van de code gewijzigd), werden er zeer weinig regressies gerapporteerd. Hoewel we talloze problemen ondervonden bij het migreren van de eerste paar bestanden, hadden we na een tijdje een solide, gedeeltelijk geautomatiseerde workflow. Dit betekende dat de negatieve gebruikersimpact voor onze stabiele gebruikers minimaal was voor deze migratie.
  5. Het is moeilijk en soms onmogelijk om de fijne kneepjes van een bepaalde migratie aan collega-beheerders te leren. Migraties van deze omvang zijn lastig te volgen en vergen veel domeinkennis. Het overdragen van die domeinkennis aan anderen die in dezelfde codebase werken, is op zichzelf niet wenselijk voor het werk dat zij doen. Weten wat je moet delen en welke details je niet moet delen is een kunst, maar wel noodzakelijk. Het is daarom van cruciaal belang om het aantal grote migraties terug te dringen, of in ieder geval niet tegelijkertijd uit te voeren.

Download de voorbeeldkanalen

Overweeg het gebruik van Chrome Canary , Dev of Beta als uw standaard ontwikkelingsbrowser. Deze preview-kanalen geven u toegang tot de nieuwste DevTools-functies, testen geavanceerde webplatform-API's en ontdekken problemen op uw site voordat uw gebruikers dat doen!

Neem contact op met het Chrome DevTools-team

Gebruik de volgende opties om de nieuwe functies en wijzigingen in het bericht te bespreken, of iets anders gerelateerd aan DevTools.

  • Stuur ons een suggestie of feedback via crbug.com .
  • Rapporteer een DevTools-probleem met behulp van de opties MeerMeer > Help > Rapporteer een DevTools-probleem in DevTools.
  • Tweet op @ChromeDevTools .
  • Laat reacties achter op onze Wat is er nieuw in DevTools YouTube-video's of DevTools Tips YouTube-video's .