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 mit HTML, CSS und JavaScript geschrieben wurde. Im Laufe der Jahre hat DevTools immer mehr Funktionen, intelligentere Funktionen und umfassendere Kenntnisse über die Webplattform erhalten. Die DevTools wurden im Laufe der Jahre zwar erweitert, ihre Architektur ähnelt jedoch weitgehend der ursprünglichen Architektur, als sie noch Teil von WebKit war.

Dieser Beitrag ist Teil einer Reihe von Blogbeiträgen, in denen die Änderungen an der Architektur und der Erstellung von DevTools beschrieben werden. Wir erklären dir, wie die Entwicklertools bisher funktioniert haben, welche Vorteile und Einschränkungen es gab und was wir getan haben, um diese Einschränkungen zu überwinden. Sehen wir uns daher die Modulsysteme, das Laden von Code und die Verwendung von JavaScript-Modulen genauer an.

Am Anfang gab es nichts

Die aktuelle Frontend-Landschaft bietet eine Vielzahl von Modulsystemen mit zugehörigen Tools sowie das jetzt standardisierte JavaScript-Modulformat. Bei der Erstveröffentlichung von DevTools gab es diese jedoch noch nicht. DevTools basiert auf Code, der ursprünglich vor mehr als 12 Jahren in WebKit veröffentlicht wurde.

Ein Modulsystem in den Entwicklertools wurde erstmals 2012 erstmals erwähnt: die Einführung einer Liste von Modulen mit einer verknüpften Liste von Quellen. Dies war Teil der Python-Infrastruktur, die damals zum Kompilieren und Erstellen der Entwicklertools verwendet wurde. Bei einer Folgeänderung wurden 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 module.json-Muster in den DevTools verwendet, um die Module und Quelldateien anzugeben. In der Zwischenzeit entwickelte sich das Web-Ökosystem rasant und mehrere Modulformate wurden entwickelt, darunter UMD, CommonJS und die schließlich standardisierten JavaScript-Module. In den Entwicklertools wurde jedoch das module.json-Format beibehalten.

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

  1. Für das module.json-Format waren benutzerdefinierte Build-Tools erforderlich, ähnlich wie bei modernen Bundlern.
  2. Es gab keine IDE-Integration, die benutzerdefinierte Tools zum Generieren von Dateien erforderten, die moderne IDEs verstehen konnten (das ursprüngliche Skript zum Generieren von jsconfig.json-Dateien für VS Code).
  3. Funktionen, Klassen und Objekte wurden alle in den globalen Bereich verschoben, um die Freigabe zwischen Modulen zu ermöglichen.
  4. Dateien waren reihenfolgeabhängig, d. h. die Reihenfolge, in der sources aufgeführt waren, war wichtig. Es gab keine Garantie dafür, dass der Code, auf den Sie angewiesen sind, geladen wird, es sei denn, ein Mensch hat ihn überprüft.

Bei der Bewertung des aktuellen Zustands des Modulsystems in DevTools und der anderen (häufiger verwendeten) Modulformate kamen wir zu dem Schluss, dass das module.json-Muster mehr Probleme verursacht als löst und dass es an der Zeit war, die Umstellung darauf zu planen.

Vorteile von Standards

Von den vorhandenen Modulsystemen haben wir uns für JavaScript-Module entschieden. Zum Zeitpunkt dieser Entscheidung wurden JavaScript-Module noch hinter einem Flag in Node.js versendet und eine große Anzahl von Paketen, die in NPM verfügbar waren, verfügten nicht über ein JavaScript-Modul-Bundle, 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. Als wir die Nachteile von module.json (siehe oben) aufgelistet haben, wurde uns klar, dass fast alle auf die Verwendung eines nicht standardisierten und einzigartigen Modulformats zurückzuführen sind.

Wenn wir ein nicht standardisiertes Modulformat auswählen, müssen wir selbst Zeit in die Erstellung von Integrationen mit den Build-Tools und Tools unserer Entwickler investieren.

Diese Integrationen waren oft instabil und es fehlte an Funktionsunterstützung. Das erforderte zusätzliche Wartungszeit und führte manchmal zu subtilen Fehlern, die letztendlich an die Nutzer weitergegeben wurden.

Da JavaScript-Module der Standard waren, konnten IDEs wie VS Code, Typprüfer wie Closure Compiler/TypeScript und Build-Tools wie Rollup/Minifier den von uns geschriebenen Quellcode verstehen. Außerdem muss ein neuer Maintainer, der dem DevTools-Team beitritt, nicht erst ein proprietäres module.json-Format lernen, während er mit JavaScript-Modulen wahrscheinlich bereits vertraut ist.

Natürlich gab es bei der ursprünglichen Entwicklung von DevTools keine der oben genannten Vorteile. Es hat Jahre gedauert, bis wir an diesem Punkt angelangt sind. Dabei haben Standardsgruppen, Laufzeitumgebungen und Entwickler, die JavaScript-Module verwenden, Feedback gegeben. Als JavaScript-Module verfügbar wurden, mussten wir eine Entscheidung treffen: Entweder unser eigenes Format weiter pflegen oder in die Migration zum neuen Format investieren.

Die Kosten für das neue

Auch wenn JavaScript-Module viele Vorteile haben, die wir gerne nutzen würden, sind wir bei der nicht standardmäßigen module.json geblieben. Um die Vorteile von JavaScript-Modulen nutzen zu können, mussten wir erhebliche Investitionen in die Beseitigung von technischem Altbestand tätigen und eine Migration durchführen, die potenziell Funktionen beeinträchtigen und Regressionsfehler verursachen konnte.

Dabei ging es nicht um die Frage „Möchten wir JavaScript-Module verwenden?“, sondern um die Frage: „Wie teuer ist es, JavaScript-Module zu verwenden?“. Hier mussten wir das Risiko von Fehlern bei der Migration für unsere Nutzer, die Kosten für die Entwickler, die viel Zeit für die Migration aufwenden, und den vorübergehenden schlechteren Zustand, in dem wir arbeiten würden, abwägen.

Dieser letzte Punkt erwies sich als sehr wichtig. Obwohl wir theoretisch zu JavaScript-Modulen gelangen könnten, würde es bei einer Migration zu Code kommen, der sowohl module.json als auch JavaScript-Module berücksichtigen müsste. Das war nicht nur technisch schwierig, sondern bedeutete auch, dass alle Entwickler, die an DevTools arbeiten, wissen mussten, wie sie in dieser Umgebung arbeiten. Sie müssen sich immer wieder die Frage stellen: „Sind es für diesen Teil der Codebasis module.json oder JavaScript-Module und wie kann ich Änderungen vornehmen?“.

Vorabinfo: Die versteckten Kosten für die Migration unserer Mitbetreuer waren höher als erwartet.

Nach der Kostenanalyse haben wir festgestellt, dass es sich dennoch lohnt, zu JavaScript-Modulen zu migrieren. Daher waren unsere Hauptziele:

  1. Achten Sie darauf, dass die Vorteile der JavaScript-Module bestmöglich genutzt werden.
  2. Die Integration in das bestehende module.json-basierte System muss sicher sein und darf keine negativen Auswirkungen auf die Nutzer haben (Regressionsfehler, Frustration der Nutzer).
  3. Alle DevTools-Verantwortlichen durch die Migration führen, vor allem mit integrierten Kontrollmechanismen, um versehentliche Fehler zu vermeiden.

Tabellen, Transformationen und technische Altlasten

Das Ziel war zwar klar, aber die Einschränkungen durch das module.json-Format erwiesen sich als schwierig, um das Problem zu umgehen. Es dauerte mehrere Iterationen, Prototypen und Architekturänderungen, bis wir eine Lösung gefunden hatten, mit der wir zufrieden waren. Wir haben ein Designdokument mit der Migrationsstrategie erstellt, die wir letztendlich umgesetzt haben. In der Designdokumentation war auch unsere ursprüngliche Zeitschätzung aufgeführt: 2 bis 4 Wochen.

Spoiler: Der intensivste Teil der Migration dauerte vier Monate und insgesamt sieben Monate.

Der ursprüngliche Plan hat sich jedoch bewährt: Wir haben die DevTools-Laufzeit angewiesen, alle Dateien, die im Array scripts in der Datei module.json aufgeführt sind, auf die alte Weise zu laden, während alle Dateien im Array modules mit dem dynamischen Import von JavaScript-Modulen geladen werden. Für jede Datei, die sich im Array modules befindet, können ES-Importe/-Exporte verwendet werden.

Außerdem würden wir die Migration in zwei Phasen durchführen (die letzte Phase wurde schließlich in zwei Teilphasen unterteilt, siehe unten): die export- und import-Phase. In einer großen Tabelle wurde der Status jedes Moduls in jeder Phase erfasst:

Tabelle zur Migration von JavaScript-Modulen

Ein Ausschnitt des Fortschrittsblatts ist hier öffentlich verfügbar.

export-Phase

In der ersten Phase werden export-Anweisungen für alle Symbole hinzugefügt, die für die verschiedenen Module/Dateien verwendet werden sollten. Die Umwandlung wird automatisiert, indem ein Script pro Ordner ausgeführt wird. Angenommen, in der module.json-Welt würde das folgende Symbol existieren:

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 unserer Quellstruktur ist das front_end/module/file1.js.)

Dies würde in Folgendes 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 beispielsweise Module.File1.localFunctionInFile in localFunctionInFile umschreiben. Wir haben jedoch festgestellt, dass es einfacher zu automatisieren und sicherer anzuwenden wäre, wenn wir diese beiden Transformationen voneinander trennen. Daher wird „Alle Symbole in dieselbe Datei migrieren“ zur zweiten Teilphase der import-Phase.

Da durch das Hinzufügen des Keywords export in eine Datei die Datei von einem „Script“ in ein „Modul“ umgewandelt wird, musste ein Großteil der DevTools-Infrastruktur entsprechend aktualisiert werden. Dazu gehörten die Laufzeit (mit dynamischem Import) und auch Tools wie ESLint, die im Modulmodus ausgeführt werden.

Bei der Behebung dieser Probleme haben wir festgestellt, dass unsere Tests im „sloppy“-Modus ausgeführt wurden. Da JavaScript-Module annehmen, dass Dateien im "use strict"-Modus ausgeführt werden, würde sich das auch auf unsere Tests auswirken. Wie sich herausstellte, beruhten eine nicht unerhebliche Anzahl von Tests auf dieser Nachlässigkeit, einschließlich eines Tests, in dem eine with-Anweisung verwendet wurde 😱.

Letztendlich dauerte es etwa eine Woche und mehrere Versuche mit Relands, bis der erste Ordner export-Anweisungen enthielt.

import-Phase

Nachdem alle Symbole sowohl mithilfe von export-Anweisungen exportiert wurden als auch im globalen Geltungsbereich geblieben sind (Legacy), mussten alle Verweise auf dateiübergreifende Symbole aktualisiert werden, um ES-Importe zu verwenden. Ziel ist es, alle „alten Exportobjekte“ zu entfernen und den globalen Umfang zu bereinigen. Die Umwandlung wird automatisiert, indem ein Script pro Ordner ausgeführt wird.

Beispielsweise für die folgenden Symbole in der module.json-Welt:

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

Sie werden in Folgendes 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();

Dieser Ansatz hatte jedoch einige Einschränkungen:

  1. Nicht jedes Symbol wurde als Module.File.symbolName benannt. Einige Symbole wurden nur Module.File oder sogar Module.CompletelyDifferentName genannt. Aufgrund dieser Inkonsistenz mussten wir eine interne Zuordnung vom alten globalen Objekt zum neu importierten Objekt erstellen.
  2. Manchmal kommt es zu Konflikten zwischen Namen auf Modulebene. Am häufigsten haben wir ein Muster verwendet, um bestimmte Arten von Events zu deklarieren, bei dem jedes Symbol einfach Events genannt wurde. Wenn Sie also in verschiedenen Dateien auf mehrere Ereignistypen lauschten, kam es in der import-Anweisung für diese Events zu einem Namenskonflikt.
  3. Wie sich herausstellte, gab es Zirkelabhängigkeiten zwischen den Dateien. Im Kontext des globalen Geltungsbereichs war dies in Ordnung, da das Symbol nach dem Laden des gesamten Codes verwendet wurde. Wenn Sie jedoch eine import benötigen, wird die zirkuläre Abhängigkeit explizit angegeben. Das ist nicht sofort ein Problem, es sei denn, Sie haben Funktionsaufrufe mit Nebenwirkungen in Ihrem Code im globalen Gültigkeitsbereich, was auch in DevTools der Fall war. Insgesamt waren einige Änderungen und Refaktorisierungen erforderlich, um die Umstellung sicher zu gestalten.

Eine ganz neue Welt mit JavaScript-Modulen

Im Februar 2020, also sechs Monate nach dem Start im September 2019, wurden die letzten Bereinigungen im Ordner ui/ durchgeführt. Damit war die Migration inoffiziell abgeschlossen. Nachdem sich die Lage beruhigt hatte, haben wir die Migration am 5. März 2020 offiziell als abgeschlossen markiert. 🎉

Jetzt verwenden alle Module in DevTools JavaScript-Module, um Code zu teilen. Wir platzieren einige Symbole weiterhin im globalen Gültigkeitsbereich (in den module-legacy.js-Dateien) für unsere Legacy-Tests oder zur Einbindung in andere Teile der DevTools-Architektur. Diese werden im Laufe der Zeit entfernt, stellen aber keine Blockade für die zukünftige Entwicklung dar. Außerdem haben wir einen Stilleitfaden für die Verwendung von JavaScript-Modulen.

Statistiken

Konservative Schätzungen für die Anzahl der Änderungslisten (Abkürzung für „Changelist“, der in Gerrit verwendete Begriff für eine Änderung, ähnlich wie ein GitHub-Pull-Request), die an dieser Migration beteiligt waren, liegen bei etwa 250 Änderungslisten, die größtenteils von zwei Entwicklern ausgeführt wurden. Wir haben keine endgültigen Statistiken zum Umfang der Ä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 etwa 30.000 (ca. 20% des gesamten Frontend-Codes der Entwicklertools).

Die erste Datei mit export wurde in Chrome 79 veröffentlicht und im Dezember 2019 als stabil veröffentlicht. Die letzte Änderung zur Migration zu import wurde in Chrome 83 eingeführt, die im Mai 2020 als stabile Version veröffentlicht wurde.

Uns ist eine Regression bekannt, die im Rahmen der Migration an Chrome (stabile Version) eingeführt wurde. Die automatische Vervollständigung von Snippets im Befehlsmenü funktioniert nicht mehr, weil ein unbefugter default-Export stattgefunden hat. Es gab mehrere weitere Regressionen, die jedoch von unseren automatisierten Test-Suites und Chrome Canary-Nutzern gemeldet wurden. Wir haben sie behoben, bevor sie die Nutzer der stabilen Chrome-Version erreichen konnten.

Den vollständigen Prozess können Sie unter crbug.com/1006759 einsehen. Es sind nicht alle CLs an diesen Fehler angehängt, die meisten jedoch sind protokolliert.

Was wir gelernt haben

  1. Entscheidungen, die in der Vergangenheit getroffen wurden, können langfristige Auswirkungen auf Ihr Projekt haben. Obwohl JavaScript-Module (und andere Modulformate) schon eine ganze Zeit lang verfügbar waren, konnte DevTools die Migration nicht rechtfertigen. Die Entscheidung, wann und wann nicht migriert werden soll, ist schwierig und basiert auf fundierten Vermutungen.
  2. Unsere ursprünglichen Zeitschätzungen waren in Wochen und nicht in Monaten angegeben. Das liegt vor allem daran, dass wir bei unserer ersten Kostenanalyse mehr unerwartete Probleme gefunden haben, als wir erwartet hatten. Obwohl der Migrationsplan solide war, waren technische Altlasten (häufiger, als wir uns gewünscht hätten) das Hindernis.
  3. Die Migration der JavaScript-Module umfasste eine große Anzahl von (scheinbar nicht zusammenhängenden) Bereinigungen technischer Altlasten. Durch die Migration zu einem modernen standardisierten Modulformat konnten wir unsere Best Practices für die Programmierung an die moderne Webentwicklung anpassen. So konnten wir beispielsweise unseren benutzerdefinierten Python-Bundler durch eine minimale Rollup-Konfiguration ersetzen.
  4. Trotz der großen Auswirkungen auf unsere Codebasis (ca. 20% des Codes geändert), wurden nur sehr wenige Regressionen gemeldet. Bei der Migration der ersten Dateien gab es zwar zahlreiche Probleme, aber nach einiger Zeit hatten wir einen soliden, teilweise automatisierten Workflow. Die negativen Auswirkungen auf unsere stabilen Nutzer waren bei dieser Migration also minimal.
  5. Es ist schwierig und manchmal unmöglich, anderen Entwicklern die Feinheiten einer bestimmten Migration beizubringen. Migrationen dieser Größenordnung sind schwierig zu verfolgen und erfordern viel Fachwissen. Die Weitergabe dieses Fachwissens an andere, die mit derselben Codebasis arbeiten, ist für die Arbeit, die sie tun, nicht per se wünschenswert. Zu wissen, was Sie teilen und welche Details Sie nicht teilen sollten, ist eine Kunst, aber eine notwendige. Daher ist es entscheidend, die Anzahl großer Migrationen zu reduzieren oder zumindest nicht gleichzeitig auszuführen.

Vorschaukanäle herunterladen

Verwenden Sie als Standard-Entwicklungsbrowser Chrome Canary, Chrome Dev oder Chrome Beta. Diese Vorabversionen bieten Zugriff auf die neuesten DevTools-Funktionen, ermöglichen den Test moderner Webplattform-APIs und helfen Ihnen, Probleme auf Ihrer Website zu finden, bevor Ihre Nutzer sie bemerken.

Chrome-Entwicklertools-Team kontaktieren

Mit den folgenden Optionen können Sie über neue Funktionen, Updates oder andere Themen im Zusammenhang mit den DevTools sprechen.