Einführung in JavaScript Source Maps

Ryan Seddon

Haben Sie sich schon einmal gewünscht, Ihren clientseitigen Code weiterhin lesbar und, was noch wichtiger ist, debuggen zu können, auch nachdem Sie ihn kombiniert und verkleinert haben, ohne die Leistung zu beeinträchtigen? Genau das ist jetzt mithilfe von Source Maps möglich.

Quellzuordnungen sind eine Möglichkeit, eine kombinierte/minimierte Datei wieder in einen noch nicht erstellten Zustand zurückzuversetzen. Wenn Sie für die Produktion entwickeln und Ihre JavaScript-Dateien reduzieren und kombinieren, wird eine Source Map mit Informationen über Ihre Originaldateien generiert. 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 so den Eindruck erwecken, als würdest du nicht komprimierte und nicht zusammengeführte Dateien ausführen.

In der Demo können Sie mit der rechten Maustaste auf eine beliebige Stelle im Textbereich mit der generierten Quelle klicken. Wenn Sie "Get original location" auswählen, wird die Source Map abgefragt, indem die generierte Zeilen- und Spaltennummer übergeben und die Position im ursprünglichen Code zurückgegeben wird. Die Konsole muss geöffnet sein, damit Sie die Ausgabe sehen können.

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

Reale Umgebung

Bevor Sie sich die folgende Implementierung von Source Maps in der Praxis ansehen, sollten Sie sich vergewissern, dass Sie die Source Maps-Funktion entweder in Chrome Canary oder in WebKit nächtlich aktiviert haben. Klicken Sie dazu im Steuerfeld für 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 und höher sind Quellzuordnungen in den integrierten Entwicklertools standardmäßig aktiviert.

So aktivieren Sie Quellzuordnungen in den Firefox-Entwicklertools.

Warum sollte ich mich um Source Maps kümmern?

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

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

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

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

Im Google Web Toolkit (GWT) wird seit Kurzem Source Maps unterstützt. Ray Cromwell vom GWT-Team hat einen großartigen Screencast erstellt, der die Source Map-Unterstützung in Aktion zeigt.

In einem weiteren Beispiel, das ich zusammengestellt habe, wird die Traceur-Bibliothek von Google verwendet, mit der Sie ES6 (ECMAScript 6 oder Next) schreiben und in ES3-kompatiblen Code kompilieren können. Der Traceur-Compiler generiert auch eine Source Map. In dieser Demo sehen Sie, wie ES6-Traits und -Klassen verwendet werden, als würden sie dank der Source Map nativ im Browser unterstützt.

Im Textbereich in der Demo können Sie auch ES6 schreiben, das sofort kompiliert wird, und eine Source Map sowie den entsprechenden ES3-Code generieren.

Traceur ES6-Fehlerbehebung mit Quellzuordnungen

Demo: ES6 schreiben, debuggen, Quellzuordnung in Aktion ansehen

Wie funktioniert die Source Map?

Der einzige JavaScript-Compiler/Minifier, 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, wird eine Source Map-Datei erstellt.

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

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

Dadurch können Entwicklertools die Aufrufe wieder ihrem Speicherort in den ursprünglichen Quelldateien zuordnen. Bisher war die Kommentar-Pragma //@. Aufgrund einiger Probleme und der bedingten IE-Kompilierungskommentare wurde entschieden, sie in //# zu ändern. Derzeit unterstützen Chrome Canary, WebKit Nightly und Firefox 24+ das neue Kommentar-Pragma. Diese Syntaxänderung betrifft auch sourceURL.

Wenn Ihnen der Kommentar nicht gefällt, können Sie auch einen speziellen Header in Ihrer kompilierten JavaScript-Datei festlegen:

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

Wie bei dem Kommentar teilt dies Ihrem Source Map-Nutzer mit, wo er nach der mit einer JavaScript-Datei verknüpften Source Map suchen kann. Mit diesem Header wird auch das Problem umgangen, auf Quellzuordnungen in Sprachen zu verweisen, die keine einzeiligen Kommentare unterstützen.

Beispiel für WebKit-Entwicklertools mit aktivierten und deaktivierten Quellzuordnungen

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

Wie generiere 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 wie folgt:

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 Closure-Compiler generiert wird, und sehen uns genauer an, wie der Abschnitt "mappings" funktioniert. Das folgende Beispiel weicht leicht vom Beispiel mit 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 interessante 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 spart außerdem Platz.
  • „sources“ enthält alle Dateinamen, die kombiniert wurden,
  • name enthält alle Variablen-/Methodennamen, die in Ihrem Code vorkommen.
  • In der Zuordnungseigenschaft geschieht dies mithilfe von Base64-VLQ-Werten. Die Platzeinsparung erfolgt hier.

Base64-VLQ und Minimieren der Source Map

Ursprünglich enthielt die Source Map-Spezifikation eine sehr ausführliche Ausgabe aller Zuordnungen, was dazu führte, dass die Source Map etwa zehnmal so groß war wie der generierte Code. Version 2 reduzierte dies um rund 50% und Version 3 wiederum um weitere 50%, sodass Sie bei einer Datei mit 133 KB eine Source Map von ca. 300 KB erhalten.

Wie wurde also die Größe reduziert, aber die komplexen Zuordnungen beibehalten?

VLQ (Variable Length Quantity) wird zusammen mit der Codierung des Werts als 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 befinden sich Kommas (,), die jedes Segment innerhalb dieser Zeile darstellen. Jedes dieser Segmente hat in Feldern mit variabler Länge den Wert 1, 4 oder 5. Einige werden möglicherweise länger angezeigt, enthalten jedoch Fortsetzungsbits. Jedes Segment baut auf dem vorherigen auf. Dadurch wird die Dateigröße reduziert, da jedes Bit relativ zu seinen vorherigen Segmenten ist.

Aufschlüsselung eines Segments in der Quellzuordnungs-JSON-Datei.

Wie bereits oben erwähnt, kann jedes Segment eine variable Länge von 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 die ursprüngliche Position ermittelt.

Die oben gezeigten Werte sind nur die Base64-decodierten Werte. Zum Abrufen der tatsächlichen Werte sind weitere Verarbeitungsschritte erforderlich. Jedes Segment umfasst in der Regel fünf Dinge:

  • Generierte Spalte
  • Originaldatei, in der diese vorkommt
  • Ursprüngliche Zeilennummer
  • Ursprüngliche Spalte
  • Und, falls verfügbar, den ursprünglichen Namen

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

Das obige Diagramm AAgBC würde nach der weiteren Verarbeitung 0, 0, 32, 16, 1 zurückgeben, wobei 32 das Fortsetzungsbit ist, mit dem der folgende Wert 16 erstellt wird. B rein in Base64 decodiert ist 1. Die wichtigen Werte, die verwendet werden, sind also 0, 0, 16, 1. Dadurch wissen wir, dass Zeile 1 (Zeilen werden durch Semikolons gezählt) Spalte 0 der generierten Datei Datei 0 zugeordnet ist (das Array der Dateien 0 ist foo.js), Zeile 16 in Spalte 1.

Um zu zeigen, wie die Segmente decodiert werden, verwende ich 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 grundlegende Kenntnisse über die bitweisen Operatoren und die Funktionsweise der Spezifikation für die Source Mapping haben. Die vorangehende Ziffer, g, wird als Fortsetzungsbit gekennzeichnet, indem die Ziffer (32) und VLQ_CONTINUATION_BIT (binär 100000 oder 32) unter Verwendung des bitweisen AND-Operators (&) verglichen werden.

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

Dadurch wird eine 1 an jeder Bitposition zurückgegeben, an der beide vorhanden sind. Ein Base64-decodierter Wert von 33 & 32 würde also 32 zurückgeben, da sie sich nur an der 32-Bit-Position teilen, wie Sie im obigen Diagramm sehen können. Dadurch wird der Bit-Verschiebungswert für jedes vorangehende Fortsetzungsbit um 5 erhöht. Im obigen Fall wird die Verschiebung nur einmal um 5 verschoben, also eine Verschiebung von 1 (B) um 5 nach links.

1 <<../ 5 // 32

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

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

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

Das war es schon: So wird eine 1 zu 16. Dieser Prozess mag kompliziert erscheinen, aber sobald die Zahlen größer werden, macht er mehr Sinn.

Mögliche XSSI-Probleme

Die Spezifikation erwähnt websiteübergreifende Einbindungsprobleme, die aus der Verwendung einer Source Map entstehen könnten. Um dies zu vermeiden, sollten Sie der ersten Zeile Ihrer Source Map „)]}“ voranstellen, um JavaScript absichtlich zu entwerten. Dadurch wird ein Syntaxfehler ausgelöst. Die WebKit-Entwicklertools können dies bereits bewältigen.

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

Wie oben dargestellt, werden die ersten drei Zeichen aufgeteilt, um zu prüfen, ob sie dem Syntaxfehler in der Spezifikation entsprechen. Ist dies der Fall, werden alle Zeichen bis zur ersten neuen Zeilenentität (\n) entfernt.

sourceURL und displayName in Aktion: Auswertungs- und anonyme Funktionen

Die folgenden beiden Konventionen sind zwar nicht Teil der Source Map-Spezifikation, ermöglichen Ihnen die Entwicklung viel einfacher, wenn Sie mit Auswertungsfunktionen und anonymen Funktionen arbeiten.

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

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

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

Mit dem anderen Helper können Sie anonyme Funktionen mithilfe der displayName-Eigenschaft benennen, die im aktuellen Kontext der anonymen Funktion verfügbar ist. Erstellen Sie ein Profil für die folgende Demo, um die displayName-Property 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 Property „displayName“ in Aktion wird gezeigt.

Bei der Profilerstellung für Ihren Code in den Entwicklertools wird das Attribut displayName anstelle von (anonymous) angezeigt. displayName ist allerdings ziemlich veraltet und wird nicht in Chrome übernommen. Dabei gehen jedoch alle Hoffnungen nicht verloren und wir haben ein viel besseres Angebot namens debugName vorgeschlagen.

Zum Zeitpunkt der Erstellung dieses Dokuments sind die Bewertungsnamen nur in Firefox- und WebKit-Browsern verfügbar. Die Eigenschaft displayName ist nur in den nächtlichen WebKit-Versionen enthalten.

Gemeinsame Zeitung

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

Bei UglifyJS ist außerdem ein Problem mit der Source Map aufgetreten, das Sie sich ebenfalls ansehen sollten.

Viele tools generieren Source Maps, einschließlich des Coffeescript-Compilers. Ich betrachte dies jetzt für einen Streitpunkt.

Je mehr Tools uns zur Verfügung stehen, um Source Maps zu generieren, desto besser werden wir. Also legen Sie los und stellen Sie Ihrem bevorzugten Open-Source-Projekt Unterstützung für Source Map zur Verfügung.

Die Informationen sind nicht perfekt

Eine Sache, die Source Maps derzeit nicht abdeckt, sind Watch-Ausdrücke. Das Problem besteht darin, dass der Versuch, ein Argument oder einen Variablennamen im aktuellen Ausführungskontext zu überprüfen, nichts zurückgibt, da es nicht wirklich existiert. Dies würde eine Art umgekehrter Zuordnung erfordern, um den echten Namen des zu prüfenden Arguments bzw. der zu überprüfenden Variable im Vergleich zum tatsächlichen Argument-/Variablennamen in Ihrem kompilierten JavaScript nachzuschlagen.

Dieses Problem ist natürlich lösbar und wenn Source Maps mehr Aufmerksamkeit erhalten, können wir einige erstaunliche Funktionen und eine bessere Stabilität feststellen.

Probleme

Seit Kurzem unterstützt jQuery 1.9 Source Maps, wenn diese außerhalb von offiziellen CDNs bereitgestellt werden. Außerdem wurde auf einen besonderen Fehler verwiesen, wenn vor dem Laden von jQuery die bedingten Kompilierungskommentare in IE (//@cc_on) verwendet wurden. Seitdem gibt es einen Commit, um dieses Problem abzuschwächen, indem die sourceMappingURL in einen mehrzeiligen Kommentar eingeschlossen wird. Verwenden Sie keine bedingten Kommentare.

Dieses Problem wurde in der Zwischenzeit durch die Änderung der Syntax in //# angegangen.

Tools und Ressourcen

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

  • Nick Fitzgerald hat eine Abspaltung von UglifyJS mit Unterstützung für Source Map.
  • Paul Irish hat eine praktische kleine Demo erstellt, in der Source Maps gezeigt werden.
  • Sehen Sie sich das WebKit-Changeset an, um zu erfahren, wann dies Drop wurde.
  • Das Changeset enthielt auch einen Layouttest, mit dem der gesamte Artikel begann.
  • Mozilla hat einen Programmfehler, den Sie in Bezug auf den Status der Quellzuordnungen in der integrierten Konsole verfolgen sollten.
  • Conrad Irwin hat für alle Ruby-Nutzer ein sehr nützliches Source Map-Gem geschrieben.
  • Weitere Informationen zur Auswertung und zum displayName-Attribut
  • Sie können sich die Quelle von Closure Compilers zum Erstellen von Source Maps ansehen
  • Hier sind einige Screenshots und wir sprechen über die Unterstützung für GWT-Quellzuordnungen.

Quellzuordnungen sind ein sehr leistungsstarkes Dienstprogramm im Toolsatz eines Entwicklers. Es ist äußerst nützlich, Ihre Web-App schlank, aber leicht Debug-fähig zu halten. Es ist auch ein sehr leistungsstarkes Lerntool für neue Entwickler, mit dem sie sehen können, wie erfahrene Entwickler ihre Apps strukturieren und schreiben, ohne unleserlichen komprimierten Code durchforsten zu müssen.

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