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 wurden die DevTools immer funktionsreicher, intelligenter und besser über die Webplattform informiert. 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, wie DevTools bisher funktioniert hat, welche Vorteile und Einschränkungen es gab und was wir unternommen haben, um diese Einschränkungen zu beheben. Sehen wir uns daher genauer an, wie Module funktionieren, wie Code geladen wird und warum wir uns für JavaScript-Module entschieden haben.

Am Anfang war 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 vor über 12 Jahren in WebKit eingeführt wurde.

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

module.json-Beispieldatei:

{
  "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 hat sich das Web-Ökosystem rasant weiterentwickelt und es wurden mehrere Modulformate erstellt, darunter UMD, CommonJS und die schließlich standardisierten JavaScript-Module. In den Entwicklertools wurde jedoch das module.json-Format beibehalten.

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

  1. Für das module.json-Format war benutzerdefinierte Build-Tool-Software erforderlich, ähnlich wie bei modernen Bundlern.
  2. Es gab keine IDE-Integration, was benutzerdefinierte Tools erforderte, um Dateien zu generieren, die von modernen IDEs verstanden werden konnten (das ursprüngliche Script 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, es aufzugeben.

Vorteile von Standards

Von den vorhandenen Modulsystemen haben wir uns für JavaScript-Module entschieden. Zum Zeitpunkt dieser Entscheidung wurden JavaScript-Module noch in Node.js mit einem Flag bereitgestellt und viele der auf NPM verfügbaren Pakete hatten kein 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 der module.json aufgelistet haben (siehe oben), haben wir festgestellt, dass fast alle mit der Verwendung eines nicht standardisierten und einzigartigen Modulformats zusammenhängen.

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, da er wahrscheinlich bereits mit JavaScript-Modulen 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 brandneue

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 Bereinigung technischer Altlasten tätigen und eine Migration durchführen, die potenziell Funktionen beeinträchtigen und Regressionsfehler verursachen konnte.

Es ging nicht mehr darum, ob wir JavaScript-Module verwenden möchten, sondern wie teuer es ist, JavaScript-Module verwenden zu können. 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. Auch wenn wir theoretisch JavaScript-Module verwenden könnten, würden wir bei einer Migration Code erhalten, 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üssten sich ständig fragen: „Handelt es sich bei diesem Teil der Codebasis um module.json- oder JavaScript-Module und wie führe ich Änderungen durch?“

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 Sie die Vorteile der Verwendung von JavaScript-Modulen so weit wie möglich nutzen.
  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 des module.json-Formats ließen sich nur schwer 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 verwendet 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 im modules-Array 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:

Migrationstabelle für JavaScript-Module

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 zwischen Modulen/Dateien gemeinsam genutzt werden sollen. 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');
};

Dabei ist Module der Name des Moduls und File1 der Name der Datei. In unserem Sourcetree-Verzeichnis wäre das front_end/module/file1.js.)

Das würde zu folgendem Ergebnis führen:

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

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

Ursprünglich war geplant, in dieser Phase auch Importe derselben Datei neu zu schreiben. 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 dies auch auf unsere Tests auswirken. Wie sich herausstellte, beruhte 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 mit export-Anweisungen exportiert und im globalen Gültigkeitsbereich (alt) geblieben waren, mussten wir alle Verweise auf dateiübergreifende Symbole aktualisieren, 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. Das war in einem globalen Kontext in Ordnung, da das Symbol erst nach dem Laden des gesamten Codes verwendet wurde. Wenn Sie jedoch einen import benötigen, wird der Zirkelbezug explizit gemacht. 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 genauen Statistiken zur Größe 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) liegt bei etwa 30.000 (ca. 20% des gesamten DevTools-Frontend-Codes).

Die erste Datei mit export wurde in Chrome 79 eingeführt, das im Dezember 2019 als stabile Version veröffentlicht wurde. 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 in der stabilen Chrome-Version eingeführt wurde und im Rahmen dieser Migration aufgetreten ist. 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 Ablauf (nicht alle CLs sind mit diesem Fehler verknüpft, aber die meisten) finden Sie unter crbug.com/1006759.

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 seit einiger Zeit verfügbar waren, konnten die 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, war technische Altlast (häufiger als uns lieb war) der Flaschenhals.
  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-Bündelungstool durch eine minimale Rollup-Konfiguration ersetzen.
  4. Trotz der großen Auswirkungen auf unsere Codebasis (ungefähr 20% des Codes wurden 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 wichtig, die Anzahl der großen Migrationen zu reduzieren oder sie zumindest nicht gleichzeitig auszuführen.

Vorschaukanäle herunterladen

Verwenden Sie als Standard-Entwicklungsbrowser Chrome Canary, Chrome Dev oder Chrome Beta. Diese Vorabversionen bieten Ihnen Zugriff auf die neuesten DevTools-Funktionen, ermöglichen es Ihnen, innovative Webplattform-APIs zu testen, 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.