Einführung in JavaScript Source Maps

Ryan Seddon

Haben Sie sich jemals gewünscht, Ihren clientseitigen Code lesbar und, was noch wichtiger ist, debuggen zu können, auch nachdem Sie ihn zusammengeführt und reduziert haben, ohne die Leistung zu beeinträchtigen? Genau das ist jetzt dank Source Maps möglich.

Quellzuordnungen sind eine Möglichkeit, eine kombinierte/komprimierte Datei einem nicht erstellten Zustand zuzuordnen. Beim Erstellen für die Produktion generieren Sie mit dem Komprimieren und Kombinieren Ihrer JavaScript-Dateien eine Source Map, die Informationen zu Ihren Originaldateien enthält. Wenn Sie eine bestimmte Zeilen- und Spaltennummer in Ihrem generierten JavaScript abfragen, können Sie eine Suche in der Source Map durchführen, die die ursprüngliche Position zurückgibt. Entwicklertools (derzeit nächtliche WebKit-Builds, Google Chrome oder Firefox 23+) können die Source Map automatisch parsen und es so aussehen lassen, als würden Sie nicht komprimierte und nicht kombinierte Dateien ausführen.

In der Demo können Sie mit der rechten Maustaste auf eine beliebige Stelle im Textbereich mit der generierten Quelle klicken. Wählen Sie "Get original location" (Ursprünglichen Standort abrufen) aus, um die Source Map abzufragen, indem die generierte Zeilen- und Spaltennummer übergeben wird. Anschließend wird die Position im ursprünglichen Code zurückgegeben. Achten Sie darauf, dass die Konsole geöffnet ist, damit Sie die Ausgabe sehen können.

Beispiel für die Verwendung der Mozilla JavaScript Source Map-Bibliothek in Aktion.

Praxis

Bevor Sie sich die folgende reale Implementierung von Source Maps ansehen, vergewissern Sie sich, dass Sie die Source Maps-Funktion entweder in Chrome Canary oder in einem nächtlichen WebKit aktiviert haben. Klicken Sie dazu im Steuerfeld der Entwicklertools auf das Zahnradsymbol für die Einstellungen und aktivieren Sie die Option "Source Maps aktivieren".

So aktivieren Sie Source Maps in WebKit-Entwicklertools.

In Firefox 23+ sind Source Maps standardmäßig in den integrierten Entwicklertools aktiviert.

Aktivieren von Source Maps in den Firefox-Entwicklertools

Warum sollte ich mich für Source Maps interessieren?

Derzeit funktioniert Source Mapping nur zwischen unkomprimiertem/kombiniertem JavaScript und komprimiertem/nicht kombiniertem JavaScript, aber die Zukunft sieht rosig aus, mit Ankündigungen zu kompilierten JavaScript-Sprachen wie CoffeeScript und der Möglichkeit, Unterstützung für CSS-Präprozessoren wie SASS oder LESS hinzuzufügen.

Künftig könnten wir mit Source Maps ganz einfach fast jede Sprache so verwenden, als würde sie nativ im Browser unterstützt:

  • CoffeeScript
  • ECMAScript 6 und höher
  • SASS/LESS und andere
  • So gut wie jede Sprache, die in JavaScript kompiliert wird

Sehen Sie sich diesen Screencast von CoffeeScript in einem experimentellen Build der Firefox-Konsole an:

Das Google Web Toolkit (GWT) bietet seit Kurzem Unterstützung für Source Maps. Ray Cromwell vom GWT-Team hat in einem großartigen Screencast die Unterstützung von Source Maps in Aktion gezeigt.

Ein weiteres Beispiel, das ich zusammengestellt habe, verwendet die Bibliothek Traceur von Google, mit der Sie ES6 (ECMAScript 6 oder Next) schreiben und in ES3-kompatiblen Code kompilieren können. Der Compiler Traceur generiert ebenfalls eine Source Map. Sehen Sie sich diese Demo an, in der gezeigt wird, wie die ES6-Traits und -Klassen dank der Source Map so verwendet werden, als würden sie im Browser nativ unterstützt.

Der Textbereich in der Demo ermöglicht Ihnen auch, ES6 zu schreiben, das im laufenden Betrieb kompiliert wird und eine Source Map sowie den entsprechenden ES3-Code generiert.

Traceur ES6-Debugging mit Quellzuordnungen

Demo: ES6 schreiben, Fehler beheben, Quellzuordnung in Aktion ansehen

Wie funktioniert die Source Map?

Der einzige JavaScript-Compiler/Komprimator, der derzeit die Generierung von Source Maps unterstützt, ist der Compiler Closure. (Die Verwendung erkläre ich später.) Sobald Sie Ihr JavaScript kombiniert und komprimiert haben, befindet sich daneben eine Source Map-Datei.

Derzeit fügt der Compiler Closure den speziellen Kommentar am Ende nicht hinzu, der erforderlich ist, um den Entwicklertools eines Browsers mitzuteilen, dass eine Source Map verfügbar ist:

//# sourceMappingURL=/path/to/file.js.map

So können Entwicklertools die Aufrufe zurück zu ihrem Speicherort in den ursprünglichen Quelldateien zuordnen. Früher betrug das Kommentar-Pragma //@. Aufgrund einiger Probleme und der bedingten Kompilierung in IE wurde jedoch entschieden, es in //# zu ändern. Derzeit unterstützen Chrome Canary, WebKit Nightly und Firefox 24+ das neue Kommentarpragma. Diese Syntaxänderung wirkt sich auch auf „sourceURL“ aus.

Wenn Ihnen die Idee des seltsamen Kommentars nicht gefällt, können Sie Ihrer kompilierten JavaScript-Datei auch einen speziellen Header hinzufügen:

X-SourceMap: /path/to/file.js.map

Wie beim Kommentar teilt dieser dem Nutzer der Source Map mit, wo er nach der mit einer JavaScript-Datei verknüpften Source Map suchen kann. In diesem Header wird auch das Problem behoben, dass auf Quellzuordnungen in Sprachen verwiesen wird, die keine einzeiligen Kommentare unterstützen.

Beispiel für WebKit-Entwicklertools für aktivierte Quellzuordnungen und deaktivierte Source Maps.

Die Source Map-Datei wird nur heruntergeladen, wenn Sie Source Maps aktiviert haben und Ihre Entwicklertools geöffnet sind. Außerdem müssen Sie die Originaldateien hochladen, damit die Entwicklertools sie referenzieren und bei Bedarf anzeigen können.

Wie erstelle ich eine Source Map?

Sie müssen den Closure-Compiler verwenden, um Ihre JavaScript-Dateien zu komprimieren, zu verketten und eine Source Map zu generieren. Der Befehl lautet:

java -jar compiler.jar \
--js script.js \
--create_source_map ./script-min.js.map \
--source_map_format=V3 \
--js_output_file script-min.js

Die beiden wichtigen Befehls-Flags sind --create_source_map und --source_map_format. Dies ist erforderlich, da die Standardversion V2 ist und wir nur mit V3 arbeiten möchten.

Aufbau einer Source Map

Zum besseren Verständnis einer Source Map nehmen wir ein kleines Beispiel für eine Source Map-Datei, die vom Compiler Closure generiert wird, und gehen genauer auf die Funktionsweise des Abschnitts "mappings" ein. Das folgende Beispiel weicht leicht vom Beispiel der V3-Spezifikation ab.

{
    version : 3,
    file: "out.js",
    sourceRoot : "",
    sources: ["foo.js", "bar.js"],
    names: ["src", "maps", "are", "fun"],
    mappings: "AAgBC,SAAQ,CAAEA"
}

Oben sehen Sie, dass eine Source Map ein Objektliteral ist, das viele aussagekräftige Informationen enthält:

  • Versionsnummer, auf der die Source Map basiert
  • Der Dateiname des generierten Codes (Ihre komprimierte/kombinierte Produktionsdatei)
  • Mit „sourceRoot“ können Sie den Quellen eine Ordnerstruktur voranstellen. Dies ist auch eine platzsparende Technik.
  • "sources" enthält alle Dateinamen, die kombiniert wurden.
  • name enthält alle Variablen-/Methodennamen, die im gesamten Code vorkommen.
  • Und schließlich erfolgt die Magie mithilfe der Eigenschaft „mappings“ mithilfe von Base64-VLQ-Werten. Hier können Sie Platz sparen.

Base64-VLQ und kleine Source Map

Ursprünglich hatte die Source Map-Spezifikation eine sehr ausführliche Ausgabe aller Zuordnungen, was dazu führte, dass die Source Map ungefähr zehnmal so groß war wie der generierte Code. In Version 2 wurde dies um etwa 50% reduziert, in Version 3 noch einmal um weitere 50 %. Bei einer Datei mit 133 KB erhalten Sie also eine Source Map mit ca. 300 KB.

Wie wurde also die Größe reduziert und gleichzeitig die komplexen Zuordnungen beibehalten?

VLQ (Variable Length Quantity) wird zusammen mit der Codierung des Werts in einen Base64-Wert verwendet. Die Eigenschaft „mappings“ ist ein sehr großer String. Innerhalb dieser Zeichenfolge befinden sich Semikolons (;), die eine Zeilennummer in der generierten Datei darstellen. Innerhalb jeder Zeile stehen Kommas (,), die die einzelnen Segmente innerhalb dieser Zeile darstellen. Jedes dieser Segmente besteht aus Feldern mit variabler Länge, 1, 4 oder 5. Einige erscheinen möglicherweise länger, enthalten jedoch Fortsetzungsbits. Jedes Segment baut auf dem vorherigen auf, was zur Reduzierung der Dateigröße beiträgt, da jedes Bit im Verhältnis zu seinen vorherigen Segmenten steht.

Aufschlüsselung eines Segments in der Source Map-JSON-Datei.

Wie oben erwähnt, kann jedes Segment eine variable Länge 1, 4 oder 5 haben. Dieses Diagramm wird als variable Länge von vier mit einem Fortsetzungsbit (g) betrachtet. Wir schlüsseln dieses Segment auf und zeigen Ihnen, wie die Source Map den ursprünglichen Standort ermittelt.

Bei den oben gezeigten Werten handelt es sich nur um die decodierten Base64-Werte. Es sind weitere Verarbeitungsschritte erforderlich, um die tatsächlichen Werte zu ermitteln. Jedes Segment liefert normalerweise fünf Dinge:

  • Generierte Spalte
  • Originaldatei, in der diese Datei vorkam
  • Ursprüngliche Zeilennummer
  • Ursprüngliche Spalte
  • Und, falls verfügbar, der ursprüngliche Name

Nicht jedes Segment hat einen Namen, Methodennamen oder ein Argument. Daher wechseln die Segmente überall zwischen der variable Länge vier und fünf. Der g-Wert im obigen Segmentdiagramm ist ein sogenanntes Continuation-Bit, das eine weitere Optimierung in der Base64-VLQ-Decodierungsphase ermöglicht. Mit einem Fortsetzungsbit können Sie auf einem Segmentwert aufbauen, sodass Sie große Zahlen speichern können, ohne eine große Zahl speichern zu müssen. Diese äußerst intelligente platzsparende Technik hat ihren Ursprung im MIDI-Format.

Nach der weiteren Verarbeitung des Diagramms AAgBC gibt das Diagramm 0, 0, 32, 16, 1 zurück, wobei 32 das Fortsetzungsbit ist, mit dem der Wert 16 erstellt wird. B, das in Base64 nur decodiert wird, ist 1. Die wichtigen Werte, die verwendet werden, sind also 0, 0, 16, 1. Dadurch wissen wir, dass Zeile 1 (die Zeilen werden durch Semikolons gezählt) Spalte 0 der generierten Datei Datei 0 zugeordnet ist (Array der Dateien 0 ist foo.js), Zeile 16 in Spalte 1.

Um zu zeigen, wie die Segmente decodiert werden, verweise ich auf die Source Map JavaScript-Bibliothek von Mozilla. Sie können sich auch den Quellcode der WebKit-Entwicklertools ansehen, der ebenfalls in JavaScript geschrieben ist.

Um richtig zu verstehen, wie wir den Wert 16 von B erhalten, müssen wir ein grundlegendes Verständnis von bitweisen Operatoren haben und wissen, wie die Spezifikation für Source Mapping funktioniert. Die vorangehende Ziffer g wird als Fortsetzungsbit gekennzeichnet, indem die Ziffer (32) und VLQ_CONTINUATION_BIT (binär 100000 oder 32) mit dem bitweisen UND-Operator (&) verglichen werden.

32 & 32 = 32
// or
100000
|
|
V
100000

Dies gibt eine 1 an jeder Bitposition zurück, in der beide enthalten sind. Ein Base64-decodierter Wert von 33 & 32 würde also 32 zurückgeben, da sie nur die 32-Bit-Position teilen, wie im obigen Diagramm dargestellt. Dadurch wird der Bitverschiebungswert für jedes vorangehende Fortsetzungsbit um 5 erhöht. Im obigen Fall wird nur einmal um 5 verschoben, also nach links, also 1 (B) um 5.

1 <<../ 5 // 32

// Shift the bit by 5 spots
______
|    |
V    V
100001 = 100000 = 32

Dieser Wert wird dann von einem VLQ-Wert mit Vorzeichen umgewandelt, indem die Zahl (32) um einen Punkt nach rechts verschoben wird.

32 >> 1 // 16
//or
100000
|
 |
 V
010000 = 16

Das war's auch schon. So wird aus 1 16. Dieser Prozess mag kompliziert erscheinen, aber sobald die Zahlen größer werden, macht er mehr Sinn.

Mögliche XSSI-Probleme

In der Spezifikation werden Probleme mit der Einbeziehung von Cross-Site-Scripts erwähnt, die aus dem Verbrauch einer Source Map entstehen können. Um dieses Problem zu beheben, sollten Sie der ersten Zeile Ihrer Source Map „)]}“ voranstellen, um JavaScript absichtlich zu entwerten, sodass ein Syntaxfehler ausgelöst wird. Die WebKit-Entwicklertools können dies bereits handhaben.

if (response.slice(0, 3) === ")]}") {
    response = response.substring(response.indexOf('\n'));
}

Wie oben gezeigt, werden die ersten drei Zeichen aufgeteilt, um zu überprüfen, ob sie mit dem Syntaxfehler in der Spezifikation übereinstimmen. Falls ja, werden alle Zeichen bis zur ersten neuen Zeilenentität (\n) entfernt.

sourceURL und displayName in Aktion: Auswertungs- und anonyme Funktionen

Obwohl die folgenden beiden Konventionen nicht Teil der Source Map-Spezifikation sind, können Sie die Entwicklung bei der Arbeit mit Auswertungen und anonymen Funktionen erheblich vereinfachen.

Das erste Hilfsprogramm sieht der Eigenschaft //# sourceMappingURL sehr ähnlich und wird tatsächlich in der Source Map V3-Spezifikation erwähnt. Indem Sie den folgenden speziellen Kommentar in Ihren Code aufnehmen, der ausgewertet wird, können Sie Evaluatoren benennen, sodass sie in Ihren Entwicklertools als logischere Namen erscheinen. Sehen Sie sich eine einfache Demo mit dem CoffeeScript-Compiler an:

Demo: Code von eval() als Script über die Quell-URL anzeigen

//# sourceURL=sqrt.coffee
So sieht der spezielle sourceURL-Kommentar in Entwicklertools aus

Mit dem anderen Hilfsprogramm können Sie anonyme Funktionen mithilfe der displayName-Eigenschaft benennen, die im aktuellen Kontext der anonymen Funktion verfügbar ist. Erstellen Sie ein Profil der folgenden Demo, um die displayName-Eigenschaft in Aktion zu sehen.

btns[0].addEventListener("click", function(e) {
    var fn = function() {
        console.log("You clicked button number: 1");
    };

    fn.displayName = "Anonymous function of button 1";

    return fn();
}, false);
Die Eigenschaft „displayName“ in Aktion.

Wenn Sie Ihren Code in den Entwicklertools erstellen, wird das Attribut displayName und nicht etwa (anonymous) angezeigt. displayName ist jedoch nahezu vollständig überflüssig und wird nicht in Chrome übernommen. Aber es ist noch nicht alles verloren. Es wurde ein viel besserer Vorschlag namens debugName vorgeschlagen.

Zum Zeitpunkt der Abfassung dieses Artikels war die Auswertungsbenennung nur für Firefox- und WebKit-Browser verfügbar. Die Eigenschaft displayName ist nur in nächtlichen WebKit-Anwendungen verfügbar.

Gemeinsam sind wir dran

Derzeit wird umfassend über die Unterstützung von Source Maps in CoffeeScript gesprochen. Schauen Sie sich das Problem an und fügen Sie Ihre Unterstützung für das Hinzufügen der Source Maps-Generierung zum CoffeeScript-Compiler hinzu. Dies wird für CoffeeScript und seine treuen Follower ein großer Gewinn sein.

Bei UglifyJS liegt auch ein Fehler mit Quellzuordnung vor, den Sie sich ebenfalls ansehen sollten.

Viele tools generieren Source Maps, einschließlich des CoffeeScript-Compilers. Ich halte das jetzt für einen irrelevanten Punkt.

Je mehr Tools uns zur Verfügung stehen, mit denen Sie Source Maps erstellen können, desto besser sind wir. Sie können also gleich loslegen und Source Maps-Unterstützung für Ihr bevorzugtes Open-Source-Projekt hinzufügen.

Nicht perfekt

Eine Sache, die Source Maps derzeit nicht unterstützen, sind Watch-Ausdrücke. Das Problem besteht darin, dass beim Versuch, ein Argument oder einen Variablennamen im aktuellen Ausführungskontext zu prüfen, nichts zurückgegeben wird, da diese nicht wirklich vorhanden sind. Dazu ist eine Art von umgekehrter Zuordnung erforderlich, um den echten Namen des Arguments/der Variablen, die Sie überprüfen möchten, im Vergleich zum tatsächlichen Argument-/Variablennamen in Ihrem kompilierten JavaScript nachzuschlagen.

Dies ist natürlich ein lösbares Problem und wenn wir uns mehr auf Source Maps konzentrieren, werden wir einige erstaunliche Funktionen und eine bessere Stabilität sehen.

Probleme

Seit Kurzem unterstützt jQuery 1.9 Source Maps für die Bereitstellung von externen CDNs. Außerdem weist er auf einen eigenartigen Programmfehler hin, wenn IE-Kommentare zur bedingten Kompilierung (//@cc_on) vor dem Laden von jQuery verwendet wurden. Zur Behebung dieses Problems wurde inzwischen ein Commit durchgeführt, indem die "sourceMappingURL" in einen mehrzeiligen Kommentar eingebettet wurde. Eine wichtige Lektion sollten Sie keine bedingten Kommentare verwenden.

Dieses Problem wurde inzwischen durch die Änderung der Syntax in //# umgangen.

Tools und Ressourcen

Hier finden Sie weitere Ressourcen und Tools, die Sie sich ansehen sollten:

  • Nick Fitzgerald hat eine Fork von UglifyJS mit Source Maps-Unterstützung.
  • Paul Irish hat eine praktische kleine Demo mit Source Maps veröffentlicht
  • Sehen Sie sich das WebKit-Changeset dazu an, wann dies verworfen wurde
  • Im Changeset war auch ein Layouttest enthalten, mit dem der Artikel begann.
  • Mozilla hat einen Fehler, den Sie im Hinblick auf den Status von Source Maps in der integrierten Konsole verfolgen sollten.
  • Conrad Irwin hat ein äußerst nützliches Source Map-Gem für alle Ruby-Nutzer geschrieben.
  • Weitere Informationen zur Auswertungsbenennung und zum Attribut „displayName“
  • Informationen zum Erstellen von Quellzuordnungen finden Sie in der Closure Compilers-Quelle.
  • Es gibt einige Screenshots und die Unterstützung für GWT-Source Maps.

Source Maps sind ein sehr leistungsstarkes Dienstprogramm in den Tools von Entwicklern. Es ist sehr hilfreich, eine schlanke Web-App zu gestalten, die leicht zu debuggen ist. Es ist auch ein sehr leistungsfähiges Lerntool für neuere Entwickler, um zu sehen, wie erfahrene Entwickler ihre Apps strukturieren und schreiben, ohne unlesbaren komprimierten Code durchsehen zu müssen.

Worauf wartest du noch? Beginnen Sie jetzt mit dem Generieren von Quellzuordnungen für alle Projekte.