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:
- Het
module.json
-formaat vereiste op maat gemaakte tools, vergelijkbaar met moderne bundelaars. - 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 ).
- Functies, klassen en objecten zijn allemaal op de globale scope geplaatst om het delen tussen modules mogelijk te maken.
- 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:
- Zorg ervoor dat het gebruik van JavaScript-modules zoveel mogelijk voordelen oplevert.
- Zorg ervoor dat de integratie met het bestaande op
module.json
gebaseerde systeem veilig is en niet leidt tot negatieve gevolgen voor de gebruiker (regressiebugs, gebruikersfrustratie). - 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:
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
-fase
Nadat alle symbolen zijn geëxporteerd met behulp van export
-statements en binnen het globale bereik (verouderd) zijn gebleven, 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:
- Niet elk symbool kreeg de naam
Module.File.symbolName
. Sommige symbolen heetten uitsluitendModule.File
of zelfsModule.CompletelyDifferentName
. Deze inconsistentie betekende dat we een interne mapping moesten maken van het oude globale object naar het nieuwe geïmporteerde object. - 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 naamEvents
kreeg. Dit betekende dat als u luisterde naar meerdere typen gebeurtenissen die in verschillende bestanden waren gedeclareerd, er een naamconflict zou optreden in deimport
-statement voor dieEvents
. - 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 gemeld 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
- 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.
- 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.
- 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.
- 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.
- 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 om Chrome Canary , Dev of Beta te gebruiken als uw standaard ontwikkelingsbrowser. Met deze voorbeeldkanalen krijgt u toegang tot de nieuwste DevTools-functies, kunt u geavanceerde webplatform-API's testen en kunt u problemen op uw site opsporen voordat uw gebruikers dat doen!
Neem contact op met het Chrome DevTools-team
Gebruik de volgende opties om de nieuwe functies, updates of iets anders gerelateerd aan DevTools te bespreken.
- Stuur feedback en functieverzoeken naar ons op crbug.com .
- Rapporteer een DevTools-probleem met Meer opties > Help > Rapporteer een DevTools-probleem in DevTools.
- Tweet op @ChromeDevTools .
- Laat reacties achter op Wat is er nieuw in DevTools YouTube-video's of DevTools Tips YouTube-video's .