Aktualisierung der Entwicklertools-Architektur: Migration zu JavaScript-Modulen

Tim van der Lippe
Tim van der Lippe

Wie Sie vielleicht wissen, sind die Chrome-Entwicklertools eine Webanwendung, die in HTML, CSS und JavaScript geschrieben wurde. Im Laufe der Jahre sind die Entwicklertools umfangreicher, intelligenter und umfassender geworden. Die Entwicklertools wurden zwar im Laufe der Jahre erweitert, ihre Architektur ähnelt jedoch weitgehend der ursprünglichen Architektur, als sie noch Teil von WebKit war.

Dieser Post ist Teil einer Reihe von Blogposts, in denen die Änderungen, die wir an der Architektur der Entwicklertools vornehmen, und deren Aufbau beschrieben werden. Wir erklären, wie die Entwicklertools in der Vergangenheit funktioniert haben, welche Vorteile und Einschränkungen sie haben und was wir getan haben, um diese Einschränkungen zu verringern. Beschäftigen wir uns daher eingehend mit Modulsystemen, dem Laden von Code und der Verwendung von JavaScript-Modulen.

Am Anfang gab es keine

Während die aktuelle Front-End-Landschaft eine Vielzahl von Modulsystemen mit darauf basierenden Tools und dem inzwischen standardisierten JavaScript-Modulformat umfasst, gab es bei der Entwicklung der Entwicklertools keines dieser Systeme. DevTools basiert auf Code, der ursprünglich vor über 12 Jahren in WebKit verfügbar war.

Die erste Erwähnung eines Modulsystems in den Entwicklertools stammt aus dem Jahr 2012: die Einführung einer Liste von Modulen mit einer zugehörigen Liste von Quellen. Dies war Teil der Python-Infrastruktur, die damals zum Kompilieren und Erstellen von Entwicklertools verwendet wurde. Eine weitere Änderung hat 2013 alle Module in eine separate frontend_modules.json-Datei (Commit) und 2014 in separate module.json-Dateien (Commit) extrahiert.

Hier ein Beispiel für eine module.json-Datei:

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

Seit 2014 wird das Muster module.json in den Entwicklertools verwendet, um die Module und Quelldateien anzugeben. Inzwischen entwickelte sich das Web-Ökosystem schnell weiter und es wurden mehrere Modulformate entwickelt, darunter UMD, CommonJS und die schließlich standardisierten JavaScript-Module. Die Entwicklertools behielten jedoch das module.json-Format bei.

Die Entwicklertools funktionierten zwar nicht, hatte aber auch einige Nachteile bei der Verwendung eines nicht standardisierten und einzigartigen Modulsystems:

  1. Für das module.json-Format waren benutzerdefinierte Build-Tools erforderlich, die den modernen Bundlern ähneln.
  2. Es gab keine IDE-Integration, für die benutzerdefinierte Tools zum Generieren von Dateien erforderlich waren, die moderne IDEs verstehen konnten (ursprüngliches Skript zum Generieren von jsconfig.json-Dateien für VS Code).
  3. Alle Funktionen, Klassen und Objekte wurden auf den globalen Geltungsbereich gestellt, um die gemeinsame Nutzung zwischen Modulen zu ermöglichen.
  4. Die Dateien waren reihenfolgeabhängig, d. h., die Reihenfolge, in der sources aufgelistet wurden, war wichtig. Es gab keine Garantie, dass der Code, auf den Sie sich verlassen, geladen wird, es sei denn, er wurde von einem Menschen verifiziert.

Insgesamt kamen wir bei der Auswertung des aktuellen Status des Modulsystems in den Entwicklertools und den anderen (weiter verbreiteten) Modulformaten zu dem Schluss, dass das module.json-Muster mehr Probleme verursachte, als es gelöst hatte, und dass es an der Zeit war, uns davon zu entfernen.

Die Vorteile von Standards

Aus den bestehenden Modulsystemen haben wir JavaScript-Module für die Migration ausgewählt. Zu dieser Entscheidung wurden JavaScript-Module noch hinter einem Flag in Node.js ausgeliefert und eine große Anzahl der auf NPM verfügbaren Pakete enthielt kein JavaScript-Modulpaket, das wir verwenden konnten. Trotzdem kamen wir zu dem Schluss, dass JavaScript-Module die beste Option sind.

Der Hauptvorteil von JavaScript-Modulen besteht darin, dass es sich um das standardisierte Modulformat für JavaScript handelt. Bei der Auflistung der Nachteile von module.json (siehe oben) wurde uns klar, dass fast alle davon auf die Verwendung eines nicht standardisierten und einzigartigen Modulformats zurückzuführen sind.

Wenn wir ein nicht standardisiertes Modulformat wählen, müssen wir Zeit in die Entwicklung von Integrationen mit den Build-Tools und Tools investieren, die unsere Administratoren verwendet haben.

Diese Integrationen waren oft schwach und es fehlten die Unterstützung für Funktionen, was zusätzliche Wartungszeit erforderte, was manchmal zu geringfügigen Fehlern führte, die schließlich den Benutzern angezeigt wurden.

Da JavaScript-Module der Standard waren, bedeutete es, dass IDEs wie VS Code, Typprüfer wie Closure Compiler/TypeScript und Build-Tools wie Rollup/Minifier den von uns geschriebenen Quellcode verstehen konnten. Wenn ein neuer Administrator zum DevTools-Team stößt, muss er keine Zeit damit verbringen, ein proprietäres module.json-Format zu erlernen, da er wahrscheinlich bereits mit JavaScript-Modulen vertraut ist.

Bei der Entwicklung der Entwicklertools gab es natürlich keinen der oben genannten Vorteile. Es dauerte Jahre, in denen wir in Standardgruppen, Laufzeitimplementierungen und Entwicklern mit Feedback von JavaScript-Modulen gearbeitet haben, um zu dem Punkt zu gelangen, an dem sie jetzt liegen. Doch als die JavaScript-Module verfügbar wurden, konnten wir entweder unser eigenes Format beibehalten oder in die Migration zum neuen investieren.

Kosten für den neuen

Obwohl JavaScript-Module viele Vorteile hatten, die wir gerne nutzen würden, sind wir in der nicht standardmäßigen module.json-Welt geblieben. Da wir die Vorteile von JavaScript-Modulen nutzen konnten, mussten wir beträchtlich in die Beseitigung technischer Altlasten investieren und eine Migration durchführen, bei der Funktionen gefährdet und Regressionsfehler eingefügt werden könnten.

An dieser Stelle ging es nicht um „Möchten wir JavaScript-Module verwenden?“, sondern um die Frage „Wie teuer ist die Verwendung von JavaScript-Modulen?“. Hier mussten wir ein Gleichgewicht zwischen dem Risiko, unsere Nutzer zu zerschlagen, mit Regressionen, den Kosten der Entwickler, die (viel Zeit) für die Migration aufwenden, und dem vorübergehend schlechteren Zustand, in dem wir arbeiten würden, in Einklang bringen.

Dieser letzte Punkt hat sich als sehr wichtig herausgestellt. Obwohl wir theoretisch auf JavaScript-Module zugreifen konnten, hatten wir während einer Migration Code, der sowohl module.json- als auch JavaScript-Module berücksichtigen musste. Das war nicht nur technisch schwierig zu erreichen, sondern bedeutete auch, dass alle Entwickler, die an Entwicklertools arbeiteten, wissen mussten, wie sie in dieser Umgebung arbeiten können. Sie müssten sich dann ständig fragen: „Ist es für diesen Teil der Codebasis module.json oder JavaScript-Module und wie kann ich Änderungen vornehmen?“.

Ein kleiner Vorgeschmack: Die versteckten Kosten, die es eigentlich kosten würde, unsere Kollegen durch die Migration zu führen, war größer als erwartet.

Nach der Kostenanalyse kamen wir zu dem Schluss, dass sich die Migration zu JavaScript-Modulen immer noch lohnt. Daher waren unsere Hauptziele die folgenden:

  1. Sie stellen sicher, dass Sie durch die Verwendung von JavaScript-Modulen die größtmöglichen Vorteile erzielen.
  2. Achte darauf, dass die Einbindung in das bestehende module.json-basierte System sicher ist und keine negativen Auswirkungen auf die Nutzer hat (Regressionsfehler, Frustration bei den Nutzern).
  3. Sie leiten alle Administratoren der Entwicklertools durch die Migration, vor allem mit integrierten Checks und Balances, um versehentliche Fehler zu vermeiden.

Tabellenkalkulationen, Transformationen und technische Altlasten

Das Ziel war klar, aber die Einschränkungen des module.json-Formats erwiesen sich als schwierig zu umgehen. Es brauchte mehrere Iterationen, Prototypen und Architekturänderungen, bevor wir eine Lösung entwickelten, mit der wir vertraut waren. Wir haben ein Designdokument mit unserer Migrationsstrategie verfasst. Im Designdokument ist auch unsere erste Zeitschätzung aufgeführt: 2 bis 4 Wochen.

Achtung: Die aufwendigste Phase der Migration dauerte 4 Monate und von Anfang bis Ende sogar 7 Monate.

Der ursprüngliche Plan hat sich jedoch bewährt: Wir würden der Entwicklertools-Laufzeit beibringen, alle im scripts-Array in der module.json-Datei aufgeführten Dateien auf die alte Art und Weise zu laden, während wir alle im modules-Array aufgeführten Dateien mit JavaScript-Modulen dynamischer Import laden. Jede Datei, die sich im modules-Array befinden würde, kann ES-Importe/-Exporte verwenden.

Außerdem würden wir die Migration in zwei Phasen durchführen (wir haben die letzte Phase schließlich in zwei Unterphasen aufgeteilt, siehe unten): die export- und die import-Phase. In einer großen Tabellenkalkulation wird der Status des Moduls in welcher Phase erfasst:

Tabelle zur Migration von JavaScript-Modulen

Ein Ausschnitt der Fortschrittsanzeige ist hier öffentlich verfügbar.

export-Phase

Die erste Phase wäre das Hinzufügen von export-Anweisungen für alle Symbole, die von Modulen/Dateien gemeinsam verwendet werden sollten. Die Umwandlung würde durch Ausführung eines Skripts pro Ordner automatisiert werden. Wenn das folgende Symbol in der Welt module.json existieren würde:

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

(Hier ist Module der Name des Moduls und File1 der Name der Datei. In unserem Sourcetree ist das front_end/module/file1.js.)

Dies würde so umgewandelt:

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

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

Ursprünglich wollten wir in dieser Phase auch Importe derselben Datei umschreiben. Im obigen Beispiel würden wir Module.File1.localFunctionInFile in localFunctionInFile umschreiben. Wir stellten jedoch fest, dass es einfacher wäre, die Abläufe zu automatisieren und die Anwendung sicherer zu machen, wenn wir diese beiden Transformationen trennen würden. Daher wird „Alle Symbole in derselben Datei migrieren“ zur zweiten Unterphase der import-Phase.

Da durch das Hinzufügen des Keywords export in einer Datei die Datei von einem „Script“ in ein „Modul“ umgewandelt wird, musste ein großer Teil der Entwicklertools-Infrastruktur entsprechend aktualisiert werden. Dazu gehörte die Laufzeit (mit dynamischem Import), aber auch Tools wie ESLint zur Ausführung im Modulmodus.

Eine der Erkenntnisse, die wir bei der Behebung dieser Probleme festgestellt haben, war, dass unsere Tests in einem "unregelmäßigen" Modus durchgeführt wurden. Da JavaScript-Module implizieren, dass Dateien im "use strict"-Modus ausgeführt werden, wirkt sich dies auch auf unsere Tests aus. Wie sich herausstellte, stützen sich eine nicht unerhebliche Anzahl von Tests auf diese langsame Ausführung, darunter ein Test mit einer with-Anweisung ☀.

Am Ende dauerte die Aktualisierung des allerersten Ordners mit export-Anweisungen etwa eine Woche und mehrere Versuche mit Relands.

import-Phase

Nachdem alle Symbole sowohl mit export-Anweisungen exportiert wurden als auch im globalen Geltungsbereich (Legacy) beibehalten wurden, mussten wir alle Verweise auf dateiübergreifende Symbole aktualisieren, um ES-Importe zu verwenden. Das Ziel besteht darin, alle „Legacy-Exportobjekte“ zu entfernen und den globalen Geltungsbereich zu bereinigen. Die Umwandlung würde durch Ausführung eines Skripts pro Ordner automatisiert werden.

Zum Beispiel für die folgenden Symbole, die in der Welt module.json existieren:

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

Sie würden folgendermaßen umgewandelt:

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

Bei diesem Ansatz gab es jedoch einige Nachteile:

  1. Nicht jedes Symbol wurde als Module.File.symbolName benannt. Einige Symbole wurden ausschließlich mit Module.File oder sogar Module.CompletelyDifferentName bezeichnet. Aufgrund dieser Inkonsistenz mussten wir eine interne Zuordnung vom alten globalen Objekt zum neuen importierten Objekt erstellen.
  2. Manchmal kam es zu Konflikten zwischen moduleScoped-Namen. Vor allem haben wir ein Muster verwendet, bei dem bestimmte Typen von Events deklariert wurden, wobei jedes Symbol nur Events genannt wurde. Wenn also mehrere in verschiedenen Dateien deklarierte Ereignistypen überwacht wurden, kam es in der import-Anweisung für diese Events zu einem Namenskonflikt.
  3. Wie sich herausstellte, gab es zwischen den Dateien Kreisabhängigkeiten. In einem globalen Kontext war dies in Ordnung, da das Symbol verwendet wurde, nachdem der gesamte Code geladen wurde. Wenn Sie jedoch import benötigen, wird die zirkuläre Abhängigkeit explizit gemacht. Das ist nicht sofort ein Problem, es sei denn, es gibt Nebeneffekt-Funktionsaufrufe in deinem globalen Code, die auch bei den Entwicklertools zur Verfügung standen. Alles in allem waren einige Operationen und Refaktorierungen erforderlich, um die Transformation sicher zu machen.

Eine völlig neue Welt mit JavaScript-Modulen

Im Februar 2020, sechs Monate nach Beginn im September 2019, wurden im Ordner ui/ die letzten Bereinigungen ausgeführt. Damit war das inoffizielle Ende der Migration. Nachdem sich der Staub besiegt hatte, haben wir die Migration offiziell als abgeschlossen am 5. März 2020 markiert. 🎉

Jetzt verwenden alle Module in den Entwicklertools JavaScript-Module, um Code zu teilen. Für unsere Legacy-Tests oder zur Integration in andere Teile der Architektur der Entwicklertools haben wir weiterhin einige Symbole auf den globalen Geltungsbereich (in den module-legacy.js-Dateien) gesetzt. Sie werden im Laufe der Zeit entfernt, sind aber kein Hindernis für die zukünftige Entwicklung. Außerdem gibt es einen Styleguide für die Verwendung von JavaScript-Modulen.

Statistiken

Konservative Schätzungen für die Anzahl der an dieser Migration beteiligten CLs (Abkürzung für Changelist – der in Gerrit verwendete Begriff, der eine Änderung darstellt – ähnlich wie eine GitHub-Pull-Anfrage), die an dieser Migration beteiligt sind, beträgt etwa 250 CLs, die größtenteils von zwei Entwicklern ausgeführt werden. Wir haben keine definitiven Statistiken zum Umfang der vorgenommenen Änderungen, aber eine konservative Schätzung der geänderten Zeilen (berechnet als Summe der absoluten Differenz zwischen Einfügungen und Löschungen für jede CL) beträgt ungefähr 30.000 (etwa 20% des gesamten Frontend-Codes der Entwicklertools).

Die erste Datei mit export wurde in Chrome 79 ausgeliefert und im Dezember 2019 zur stabilen Version veröffentlicht. Die letzte Änderung zur Migration zu import wurde in Chrome 83 veröffentlicht und im Mai 2020 in die stabile Version veröffentlicht.

Uns ist eine Regression bekannt, die im Rahmen dieser Migration eingeführt wurde und die in Chrome (stabile Version) eingeführt wurde. Die automatische Vervollständigung von Snippets im Befehlsmenü ist aufgrund eines überflüssigen default-Exports fehlerhaft. Es gab mehrere andere Regressionen, aber unsere automatisierten Testsuiten und Chrome Canary-Nutzer meldeten diese Fehler und wir haben sie behoben, bevor sie stabile Chrome-Nutzer erreichen konnten.

Sie können den vollständigen Prozess unter crbug.com/1006759 einsehen (nicht alle CLs sind an diesen Fehler angehängt, aber die meisten davon sind) protokolliert.

Was wir gelernt haben

  1. In der Vergangenheit getroffene Entscheidungen können langfristige Auswirkungen auf Ihr Projekt haben. Obwohl JavaScript-Module (und andere Modulformate) schon eine Weile verfügbar waren, konnten die Entwicklertools die Migration nicht rechtfertigen. Die Entscheidung für eine Migration ist schwierig und basiert auf fundierten Vermutungen.
  2. Ursprünglich ging es dabei um Wochen und nicht um Monate. Dies ist größtenteils darauf zurückzuführen, dass wir bei unserer anfänglichen Kostenanalyse mehr unerwartete Probleme gefunden haben, als wir es erwartet hatten. Obwohl der Migrationsplan solide war, waren technische Altlasten (häufiger, als wir es uns gewünscht hätten) das Problem.
  3. Die Migration der JavaScript-Module umfasste eine große Anzahl technischer Schuldenbereinigungen, die scheinbar nichts miteinander zu tun hatten. Durch die Migration zu einem modernen standardisierten Modulformat konnten wir unsere Best Practices für das Programmieren an die moderne Webentwicklung anpassen. Beispielsweise konnten wir unseren benutzerdefinierten Python-Bundler durch eine minimale Rollup-Konfiguration ersetzen.
  4. Trotz der großen Auswirkungen auf unsere Codebasis (~20% des Codes geändert) wurden nur sehr wenige Regressionen gemeldet. Es gab zwar zahlreiche Probleme beim Migrieren der ersten Dateien, doch nach einer Weile hatten wir einen soliden, teilweise automatisierten Workflow. Dadurch waren die negativen Auswirkungen auf unsere stabilen Nutzer durch die Migration minimal.
  5. Es ist schwierig und manchmal unmöglich, anderen die Feinheiten einer Migration an andere zu vermitteln. Migrationen dieser Größenordnung sind schwer nachvollziehbar und erfordern viel Domänenwissen. Es ist an sich nicht wünschenswert, dieses Domänenwissen an andere zu übertragen, die in derselben Codebasis arbeiten. Zu wissen, was geteilt werden soll und welche Details nicht geteilt werden sollen, ist eine Kunst, sondern ein notwendiges. Daher ist es wichtig, die Anzahl großer Migrationen zu reduzieren oder zumindest nicht gleichzeitig auszuführen.

Vorschaukanäle herunterladen

Sie können Chrome Canary, Dev oder Beta als Standardbrowser für die Entwicklung verwenden. Über diese Vorschaukanäle erhältst du Zugriff auf die neuesten Entwicklertools-Funktionen, kannst neue Webplattform-APIs testen und Probleme auf deiner Website erkennen, bevor deine Nutzer es tun.

Chrome-Entwicklertools-Team kontaktieren

Verwende die folgenden Optionen, um die neuen Funktionen und Änderungen im Beitrag oder andere Themen im Zusammenhang mit den Entwicklertools zu besprechen.

  • Sende uns über crbug.com Vorschläge oder Feedback.
  • Wenn du ein Problem mit den Entwicklertools melden möchtest, klicke in den Entwicklertools auf Weitere Optionen   Mehr   > Hilfe > Probleme mit den Entwicklertools melden.
  • Senden Sie einen Tweet an @ChromeDevTools.
  • Hinterlasse Kommentare zu den Neuheiten in den Entwicklertools YouTube-Videos oder YouTube-Videos in den Entwicklertools-Tipps.