Es sollte mittlerweile in jeder halbwegs ernsthaften Webanwendung gängig sein, dass das JavaScript nicht as is ausgegeben wird, sondern in einer optimierten Variante. Dabei wird in der Regel auf eine Kombination aus Konkatenation und Minifizierung zurückgegriffen.
In diesem Artikel stelle ich eine Möglichkeit vor, dies in einem Java-Projekt mit Spring einzusetzen. Als Builder wird Maven und als JS-Compiler wird der Google Closure Compiler verwendet. Als Boni werden die Source Maps in der Konbination mit Google Chrome vorgestellt. Grundsätzlich sind einige Werkzeuge zum Teil auch ohne Java oder Spring einsetzbar — Google Closure Compiler ist zwar in Java geschrieben, aber als JAR verfügbar und universell per Executable steuerbar.
Begriffe
Bei der Konkatenation werden die einzelnen Source-Files in eine große Datei „zusammenkopiert“. Hierbei ist aufzupassen, dass die einzelnen Source-Files in sich syntaktisch korrekt sind, weil es ansonsten zu unvorhergesehenen Fehler kommt.
Die Minifizierung sorgt dafür, dass unnötige Inhalte entfernt werden (einfache Komprimierung). Je nach Einstellung und Werkzeug kann man das Ergebnis auch weiter optimieren. Die einfache Variante ist das Umbenennen von lokalen Variablen/Methoden/Funktionen in kurze Namen, die extreme Form führt sogar eine Dead Code Detection aus und schreibt gesamte Codebereiche optimiert um.
Übersicht der Werkzeuge
Es gibt zahlreiche Werkzeuge, die einen bei der Code-Minifizierung unterstützen. Einige IDEs bieten dies entweder built-in oder als Plugin an; außerdem gibt es einige Webdienste wie jscompress.com, minifyjavascript.com oder UglifyJS.
Ein Problem haben jedoch alle Varianten: Das Debugging des optimierten Codes ist praktisch unmöglich und bisweilen sehr schwierig. Dies wird jeder kennen, wenn man beispielsweise die Production-Variante von jquery.js einfügt und plötzlich beim Debuggen vor lauter Bäumen den Wald nicht mehr sieht. Zur Not springt hier wenigstens der Chrome noch zur Hilfe und bietet das Uglify-Gegenstück an: Pretty print. Die Variablen haben dann jedoch weiterhin nichtssagende Namen wie a, b oder c.
Abhilfe schaffen hier die so genannte Source Maps (siehe weiter unten).
Häufig ist für die Konkatenation jedoch wichtig, die Reihenfolge zu beachten. So können jQuery-Plugins erst geladen (und ausgeführt werden), wenn jQuery selber geladen wird. Selbst die — in sich selbst dynamischen — Ext JS-Klassen können erst definiert werden, wenn Ext JS selber geladen wurde. Daher kommt man im Regelfall nicht drumherum, eine Abhängigkeitsstruktur aufzubauen.
JavaScript Optimzer
Der als JSP-Plugin verfügbare JavaScript Optimizer bietet dafür eine eigentlich solide Grundbasis. Man definiert eine jso.xml, in der die Abhängigkeitsstruktur der JavaScript- und CSS-Files in Gruppen definiert wird. Über Gruppenreferenzen lassen sich durchaus komplexe, mehrschichtige und mehrere Zieldateien konfigurieren. Die Verwendung geschieht durch eine kleine JSP-Tag-Library.
Das Beispiel der jso.xml definiert insgesamt sechs Gruppen, wobei jedoch nur die letzten beiden für die eigentliche Verwendung gedacht sind. Mit dem JSP-Tag
werden alle JavaScript-Source-Files (und zwar alle aufgelösten, also auch die rekursiv aufgelösten aus group-refs) als Einzel-Script-Tags ausgegeben (im Ausgabe-HTML stehen dann viele einzelne HTML <script>-Tags).
Mit
geschieht eine Konkatenation aller definierten Source-Files. Da die Gruppe mit retention=“true“ definiert wurde, wird daraus beim ersten Aufruf die Datei im Root-Verzeichnis abgelegt, d.h. sie muss beim nächsten Aufruf nicht mehr erstellt werden. Dieses Verhalten ist optional, dürfte aber bei späteren Production-Environments ein guter Standard sein. Diese beide Varianten kann man beispielsweise mit Property gesteuerten „Profilen“ bedingt in eine JSP oder mehrere JSPs unterbringen.
Google Closure Compiler
Prinzipiell kann man mit diesem Vorgehen schon gut fahren, denn auch eine Minifizierung wird unterstützt. Allerdings bietet die Verwendung anderer Tools wie des Google Closure Compilers einige Vorteile gegenüber JSO:
- JSO wird leider seit einiger Zeit nicht mehr aktualisiert. Für ein solches Projekt ist der derzeitige Maintaining Status leider nicht optimal. (Zum Beispiel: Ein Adapter zum Closure Compiler ist nicht vorhanden.)
- Das Ergebnis des Google Closure Compilers ist besser, d.h. das Resultat ist kleiner und optimierter für die Ausführung.
- Der Closure Compiler kann konfiguriert werden, wie viel er optimieren soll. Das ist wichtig, weil man bei manchen Bibliotheken die aggressive Optimierung nicht benutzt werden kann.
- Die Erstellung von Source Maps (siehe weiter unten) ist derzeit nur mit dem Closure Compiler möglich.
Falls man den Google Closure Compiler einsetzen möchte, dann muss man im Build-Prozess der Anwendung das Erstellen selber erledigen. Tipp: Falls man möchte, kann man sich der vorhandenen jso.xml bedienen. Über eine einfache XML-Traversierung lässt sich diese gut „missbrauchen“. Dies wird dem geeigneten Leser als triviale Übungsaufgabe selber überlassen.
JavaScriptMVC / StealJS
StealJS, welches Bestandteil des Komplettpakets JavaScriptMVC ist, bietet eine Mischung aus beiden an. Zunächst lassen sich jeder Script-Resource über die Anweisung steal definieren, welche anderen Module für dieses Script erforderlich sind.In diesem Beispiel wird gefordert, dass jQuery zuerst geladen sein muss. Erst dann wird der Block in then ausgeführt. Dies ist eine Adaption von jQuerys $.DeferredObject und CommonJS Promises. Technisch gesehen bedeutet der Schnipsel im Übrigen: Lade zunächst die Datei jquery/jquery.js, danach führe den Callback aus. Siehe dazu die Dokumentation zu StealJS. Daneben bietet StealJS die Möglichkeit an, so genannte Production-Builds zu bauen. Auf technischer Ebene wird dabei — welch Überraschung — der Google Closure Compiler verwendet. Die Abhängigkeitsstruktur wird dabei agil über die verwendeten steal-Commands ermittelt und dann sinnvoll zusammengebaut. Das Ergebnis ist dann eine production.js, welche beide vorrangigen Ziele — also Konkatenation und Minifizierung — erfüllt. Hier muss natürlich dann auch beim Einbinden eine Weiche über eine Profilsteuerung erfolgen.
Und da waren noch die Source Maps
Die Source Maps sind eine sehr neue — yeah, wie ich immer sage: heißer Scheiß — Technik und eine Antwort darauf, wie man das allgegenwärtige Grundproblem gelöst bekommt: Wie kann man im Browser direkt erkennen, wie der echte, originale Quellcode aussieht. Also, der mit Kommentaren und eigenständigen Dateien? Und wenn wir dann schon bei den neuen, abgefahrenen Technologien sind: Wie kann ich aus dem generiertem CSS erkennen, welche SASS oder LESS-Regel dafür verantwortlich ist? Wie kann ich aus dem JavaScript erkennen, in welcher CoffeeScript-Zeile ich mich eigentlich gerade befinde? Auch wenn natürlich der generierte Code — also CSS und JavaScript — in gewisser Weise noch lesbar und verständlich ist, so arbeitet der Entwickler ja bereits in einer anderen Abstraktion. Das Debuggen in eben dieser wäre ideal.
Nun, erfreulich ist an dieser Stelle zu vermelden: Im Chrome wird es Wirklichkeit, und im Firefox tut sich auch einiges. Beide Browser werden also in diesem, wenn nicht dann spätestens nächstes, Jahr eine Unterstützung anbieten. Mit einer kleinen Ergänzung bei der Ausgabe (entweder über die HTTP-Header oder einfach über einen kleinen Code-Comment-Annotation) ist die eigene Anwendung schnell gewappnet. Darüber hinaus können auch kompakte (und minifizierte) JavaScript-Builds mit mehreren Source-Map-Annotations versehen werden, um eine sinnvolle Debugging-Grundlage zu bieten.
Das wir keine baldige Unterstützung seitens Microsoft für den IE 11 (für den IE 10 ist bereits Feature Freeze) erwarten dürften, sei mal als business as usual notiert. Der Google Closure Compiler ist schon länger in der Lage, diese Source Maps passend zum minifizierten Code zu erzeugen — derzeit in der Inkarnation der Google Source Maps v3. Sollte die Spezifikation der Source Maps nochmals geändert werden, so ist mit einer — wenn vielleicht auch nur inoffiziellen – sehr schnellen Referenzimplementierung im Closure Compiler zu rechnen.
Bereits erwähnt: CoffeeScript. Nur kurz erklärt, denn dies führe in diesem Beitrag zu weit: CoffeeScript ist wie JavaScript, nur ohne die ganzen Fallstricke. Es reduziert den Code um etliche unnötige Steuerzeichen zulasten einer peniblen Einrückung (und das ist auch gut so!) und erspart etlichen Boilercode, wenn es etwa um Schleifen oder Scope-Management geht. Dazu wird es demnächst einen eigenen Beitrag geben.
Ein großes Manko ist derzeit immer noch, dass man als CoffeeScript-Entwickler (im Bereich Web-Frontend) absolut keine Möglichkeit hat, CoffeeScript direkt zu debuggen.
Tatsächlich hat man nur zwei Möglichkeiten:
- Entweder man lässt den Watchdog laufen, der on-the-fly alle Änderungen an *.coffee-Dateien bemerkt und in *.js-Kompilate überführt. Das Ausführen und Debuggen führt dann wie gewohnt über die JavaScript-Sourcen, im Browser sind auch nur solche ersichtlich.
- Oder aber bindet CoffeeScript über den Script-Tag ein (mit type=“text/coffeescript“), dann erkennt ein vorher eingebundenes coffee-script.js die Einbindung und führt ebenfalls on-the-fly live eine Kompilierung durch. Es sei hier erwähnt, das StealJS hierfür ebenfalls einen eigenen Konverter eingebaut hat; Sourcen mit der Änderung .coffee werden automatisch kompiliert.
Die zweite Option hat jedoch den entscheidenden Nachteil, dass selbst aktuelle Firefox-Versionen nicht damit klar kommen, dass es „virtuelle“ JavaScript-Blöcke gibt. (Zum technischen Verständnis: Der Inhalt einer CoffeeScript-Datei wird durch den Compiler (der im Übrigen selber in CoffeeScript geschrieben ist) gejagt und anschließend in einem eval ausgeführt.)
Der Mehrwert von Source Maps ist also nicht zu unterschätzen — und das nur aus heutiger Sicht.
Und da war da noch: Live Editing
Ebenfalls derzeit nur für den Chrome bzw. die Chrome Dev Tools: Tincr.
Mit diesem Werkzeug kann man in Echtzeit JavaScript und CSS im Browser oder im Editor bearbeiten — und jeweils in die andere Richtung sofort aktualisieren! Zumindest abseits von Tomcat-Webanwendungen lässt sich hier die Arbeitseffizienz erheblich steigern.
Résumé
Aus dem organisatorischen Aspekt heraus kommt es darauf an, welche Vorgaben man am Projekt, an die Sourcen und am Resultat hat. Der Ansatz von JSO oder Werkzeugen ähnlichen Kalibers bieten eine transparente Entwicklungs- und Produktionsumgebung an. Mit einer kleinen Weiche beim Einfügen — importiere alle Sourcen im Profil „Developer“ oder im Profil „Production“ — lassen sich beide grundsätzlich verschiedenen Profile vereinen. Mit einem neuen Eintrag an der richtigen Stelle in der zentralen Dependency-Datei sorgt dafür, dass sowohl der Entwickler als auch der Endbenutzer die gleichen Funktionen erhält. Einmal als Einzeldatei, das andere Mal als Bestandteil einer großen optimierten Variante. Dabei ist es natürlich ein Unterschied, ob man das Dependency Management zentral steuert oder wenn jedes Modul (jede Datei) selbst die Abhängigkeiten definiert.
Unter dem technischen Aspekt ist ebenfalls die Anforderung wichtig: Möchte man Wert legen auf eine saubere und einfache Möglichkeit, Production-Umgebungen debuggable zu halten? Sowohl JSO als auch StealJS bieten dafür unterschiedliche Ansätze. Oder reicht es, wenn man etwa zum erzeugten „JavaScript-Build“ eine SourceMap erhält? Braucht man die? Und wenn ja: Zu welchem Zeitpunkt? Schon als Entwickler wie bei CoffeeScript oder erst in der Produktion nach der Minifizierung?
Quellen und weiterführende Links
- Google Closure Compiler Projekt (auf Google Code)
- Google Closure Compiler Tools (Dokumentation, FAQ, Informationen)
- „Introduction to Javascript Source Maps“ (auf html5rocks.com)
- JavaScriptMVC, StealJS (Dokumentation), StealJS (GitHub)
- RequireJS (Alternative zu StealJS)
- CoffeeScript
- „Better JS with CoffeeScript“ von Prototype-Entwickler Sam Stephenson (Vimeo-Video)
- Addy Osmani: Lets Tincr: Bi-directional Editing And Saving With The Chrome DevTools
- Tincr: Edit and save files from Chrome Developer tools. Live reload for Chrome