Mehr Probleme als Lösungen: Warum Tools wie Grunt, Gulp und Co. eine schlechte Wahl sind
Wenn du eine moderne JavaScript-Anwendung oder Websites erstellst, bist du mit hoher Wahrscheinlichkeit schon mal über die JavaScript-Build-Tool-Landschaft gestolpert. Die Schwergewichte darunter sind Grunt und Gulp, obwohl es noch viele, viele andere gibt. Grunt erzielt an den meisten Tagen kolossale 30.000 Downloads, und Gulp hat respektable 15.000 tägliche Downloads. Da müssen sie irgendwas richtig machen, stimmts? Build-Systems scheinen das angesagte Tool der JavaScript-Community zu sein. Ich habe Gulp und Grunt in ein paar Projekten eingesetzt (die meisten davon sind Closed-Source-Projekte, aber manche auch Open Source, etwa Tempus).
Für mich fühlt es sich vor allem in letzter Zeit so an, als ob viele dieser Tools die Probleme auf schlechte Art lösen. Sie alle tun es, aber in verschiedenem Ausmaß. Wenn man zwischen den Zeilen liest, kann man sehen, dass Tools wie Gulp versuchen, die Probleme von Grunt zu lösen, und Broccoli versucht, die Probleme von Gulp zu lösen – sie verdecken die Unzulänglichkeiten des vorherigen Tools, während sie ihre eigenen verbergen. Lass mich ein paar Gründe aufzählen, warum ich denke, dass diese Tools eine schlechte Wahl sind.
Grunt, Gulp und Co.: Aufgeblasen
All diese Task-Runner (oder Build-Systeme, wenn du sie so nennen willst) versuchen, mit ihren eigenen, abgeschlossenen Zauberformeln eine Art von Task-Modell zu abstrahieren. Grunt verwendet Konfigurationsdateien und Plugins über ein zentrales „Gruntfile“, das deine Einstellungen deklariert – und wie dir langjährige Grunt-Nutzer bestätigen werden, ist diese Konfiguration keineswegs knapp. Du kannst Plugins installieren, etwa grunt-contrib-jshint, die dir erlauben, den Code mit deinen Lieblings-Tools zu bearbeiten. „Wie sieht so eine Konfiguration aus?“ fragst du dich vielleicht. Naja, wenn wir ein Gruntfile bräuchten, das einfach nur jshint ausführen sollte, würde es ungefähr so aussehen:
module.exports = function(grunt) {
grunt.initConfig({
jshint: {
files: ['**.js'],
options: JSON.parse(require('fs').readFileSync('./.jshinrc'))
}
});
grunt.loadNpmTasks('grunt-contrib-jshint');
};
Und dann müsstest du natürlich noch die grunt
– und grunt-contrib-jshint
-Abhängigkeiten installieren. All das ist dazu gedacht, von den Schwierigkeiten beim Ausführen von jshint **.js
in deinem Terminal abzulenken. Natürlich muss man, um diese Grunt-Konfiguration zum Laufen zu kriegen, immer noch grunt jshint
im Terminal ausführen, was auch nicht kürzer als jshint **.js
ist. Das mag ein wenig an den Haaren herbeigezogen wirken, aber wenn man sich die Grunt-Website anschaut, kann man sehen, dass es für die Kompatibilität mit vielen deiner liebsten Plugins wirbt, so wie CoffeScript, Handlebars, Jade und viele mehr. All diese Tools installieren bereits Binaries auf deinem System (hier sind die von CoffeScript, Handlebar und Jade), und die wunderbare Ironie hinter all dem liegt darin, dass Plugins weitere Abhängigkeiten nach sich ziehen – das heißt, grunt-contrib-jshint
baut auf jshint
auf, das die Binärdatei sowieso auf dein System lädt!
Ich hacke hier nicht absichtlich auf Grunt herum, Gulp ist genauso davon betroffen. Mit Gulp benötige ich immer noch gulp-jshint und ein Gulpfile.js auf meinem Dateisystem mit dem folgenden Code:
var jshint = require('gulp-jshint');
var gulp = require('gulp');
gulp.task('jshint', function() {
return gulp.src('**.js')
.pipe(jshint())
.pipe(jshint.reporter('default'));
});
Gulps Version schlägt nur mit sieben Zeilen Code zu Buche, benötigt aber immer noch zwei Abhängigkeiten (gulp
selbst und gulp-jshint
), und ich muss immer noch gulp jshint ausführen (was nur einen Tastendruck kürzer ist als jshint **.js
). Gulp ist auch nicht der einzige Übeltäter, auch Jake, Broccoli, Brunch und Mimosa brauchen installierte Plugins. Das bedeutet, dass du mit jedem dieser Task-Runner im Grunde nur eine weitere Abhängigkeit installierst (den Task-Runner), als wenn du keinen Task-Runner hättest und nur eines der projekteigenen Binaries verwenden würdest.
All das Gerede von Plugins führt mich sehr schön zu meinem nächsten Punkt …
Wenn man sich auf Plugins verlässt
Keines dieser Build-Tools funktioniert ohne Plugins. Du hast gerade ein großartiges neues Tool gefunden, das deinen Build-Prozess revolutionieren wird? Toll! Jetzt musst du nur noch warten, bis jemand einen Wrapper für Grunt/Gulp/Broccoli schreibt, oder es selbst tun. Statt dass du dir die Kommandozeile für das Tool beibringst, das du verwenden möchtest, musst du dir nun die Programm-API plus die API für das Build-Tool beibringen. Selbst Sindre Sorhus kann nicht jedes dieser Tools für dich schreiben.
Aber das ist der Punkt, an dem du wahnsinnig wirst. Vielleicht meinst du, für manche Tasks Grunt zu benötigen und für andere Gulp oder Brunch, also betrittst du die lächerliche Welt, in der du Build-Tools benutzt, um deine Build-Tools zu managen, mit solchen Top-Hits wie grunt-brunch oder gulp-grunt, das sich in der Readme-Datei folgendermaßen beschreibt:
Was tun, wenn dein liebstes Grunt-Plugin noch nicht für Gulp verfügbar ist? Keine Angst, du musst dir keine Sorgen machen. Warum hakst du nicht einfach deine Grunt-Konfiguration mit ein?
Anstatt sich also auf die Community eines Build-Tools zu verlassen, indem du deinen neuen Lieblings-Linter für wichtig genug für ein Plugin hältst, kannst du einfach weiter Build-Tools in deinem Projekt anhäufen, bis eins davon ins Schwarze trifft.
Extra-Schmerzen beim Updaten
Mit all diesen Plugins und Tools fangen Updates an, wirklich weh zu tun. Als ich Grunt noch recht intensiv benutzte (ungefähr bei Version 0.3 und 0.4), unterzog sich Grunt einem ziemlich großen Refactoring, und dadurch funktionierte keine 0.3 Konfiguration mehr mit Grunt 0.4, also mussten die Benutzer ihre Konfigurationen migrieren. Auch Plugin-Autoren mussten ihre Plugins migrieren. Das Projekt, an dem ich zu der Zeit arbeitete, hatte ein paar Build-Scripts, die sehr dürftig von unerfahrenen Node.js-Entwicklern mit einem selbsterstellten Task-Runner umgesetzt waren – also entschied ich mich, zu Grunt zu wechseln. Gerade als ich fertig war, kam Grunt 0.4 heraus und ich verbrachte zwei weitere Wochen damit, darauf zu warten, dass alle Plugins zu 0.4 migriert waren, und aktualisierte langsam unser Gruntfile. Zugegeben, Grunt hat sich seither stark weiterentwickelt – die API ist jetzt einigermaßen stabil. Ich hatte nur Pech.
Ich habe auch mit Entwicklern gesprochen, die wegen der angepriesenen, tollen neuen Vorteile von Grunt zum nächstbesten migriert sind (zum Beispiel Gulp), und sie berichteten stolz, dass nach zwei Wochen Arbeit ihre 30 Sekunden Build-Time auf drei Sekunden runtergingen. Natürlich erlaubt mir das, elegant zum nächsten Punkt hinüberzugleiten.
Falsche Versprechen
Ich verwendete Grunt recht intenstiv, als Gulp veröffentlicht wurde. Gulp hat wirklich eine Kampfansage an Grunt gemacht – stolz verkündend, dass es keins von diesem ätzendem Konfigurationszeug mache, alles ist imperativ geschrieben, mit Code! Außerdem war alles streamend und asynchron – nicht so wie Grunt, das synchron herumgrunzt. Also probierte ich es. Ich verwendete die imperative API, um eine Handvoll von Datenstreams hochzuladen und sie an gulp-uglify weiterzugeben, wobei ich diese Message angezeigt bekam:
[gulp] Error in plugin 'gulp-uglify': Streaming not supported
Es stellte sich heraus, dass einer der angeblichen Hauptvorteile von Gulp – das Streaming-Potenzial – nicht mit allen erhältlichen Plugins funktioniert. Ich probierte noch einige Plugins mehr (JSHint funktionierte auch nicht), aber zu diesem Zeitpunkt unterstützen viele von ihnen kein Streaming. Gulp hat Plugins, um dieses Problem zu entschärfen – aber diese einzusetzen fühlt sich so an, als würde man einfach mehr Tools auf ein schlecht gelöstes Problem aufstapeln.
Gulp verspricht auch eine wirklich einfache API – „Lern fünf Befehle – dann kannst du es“. Leider sieht die Wirklichkeit ganz anders aus. Diese fünf API-Befehle verschleiern die Komplexität seines Datei-Management-Utilitys – VinylFS – und seines Task-Runner-Frameworks – Orchestrator. Beide verfügten (zu der Zeit, als ich sie benutzt habe) praktisch über keine Dokumentation, also artete der Versuch, mehr über ihre Verwendung herauszufinden, ins Durcharbeiten des Codes aus.
Schlechte Angewohnheiten
Wo ich schon über meine Erfahrungen mit Gulp spreche – als ich schließlich JSHint zum Laufen bekommen hatte und ein paar Fehler generierte, um es zu testen, fiel mir etwas auf: Es schlug nicht fehl. Das gleiche galt für Gulps Mocha-Plugin. Wenn es auf einen Fehler traf, brach Gulp mit einem Exit-Code 0
ab. Das heißt, wenn man es in eine kontinuierliche Integrations-Umgebung wie Travis oder Jenkins einfügt, würde es ein paar Fehler anzeigen und dann fortfahren. Die CI sieht nur die Log-Ausgabe und einen positiven Exit-Code und geht davon aus, dass alles in Ordnung ist. Diese Art von Hürden zu nehmen, kann eine ziemlich nervende Erfahrung sein, wenn du eigentlich nur ein paar Werkzeuge einrichten willst. Gulp hat sogar Plugins, um anständige Abbrüche zu produzieren.
Wo ich schon von schlechten Angewohnheiten spreche – ich habe mich einmal eingelesen, wie man Browserify mit Gulp zum Laufen kriegt. Offensichtlich hat Gulp das Gulp-Browserify-Plugin in seiner Registry auf die Blackist gesetzt, weil es seiner Weltanschauung nicht entsprach (es verwendet Streams, nicht Gulps vinyl-fs). Ein guter Artikel verrät ein paar Details darüber.
Die Lösung
Es wäre einfach, mich lediglich über diese Tools zu beschweren, ohne gleichzeitig eine wirkliche Alternative dazu mitzuliefern. Und es gibt defintiv eine (meiner Meinung nach). Es gibt ein Tool, das Build-Skripte ausführen und Konfigurationswerte aufnehmen kann, das Streaming unterstützt, eine unglaublich einfache API besitzt und bei jeder Node.js-Installation gratis dabei ist: npm.
Als ich Hive entwickelt habe, habe ich den Fehdehandschuh hingeworfen und zu meinen (sehr klugen) Kollegen gesagt:
Wir werden kein Grunt verwenden. Wir nutzen npm, um unsere Build-Skripte zu managen. Wenn es zu komplex wird, dann switchen wir zu Grunt und ich werde mit keiner Silbe dagegen protestieren.
Wir entwickelten das Projekt von Grund auf bis hin zum Release, und die ganze Zeit über verwendeten wir npm ohne jedes Problem. Wir wichen nie auf Grunt aus – und keiner hat sich darüber beschwert. Wir nutzten CSS-Präprozessoren, Browserify, Karma, Mocha, JSHint, Srcy und WD, allesamt ausgeführt von unserem npm-scripts
-Objekt mit ungefähr 15 Zeilen Code. Vergleiche das mal mit meinem vorherigen Job, wo wir hunderte Zeilen von Grunt-Konfiguration und Tonnen von Plugins brauchten, inklusive der selbst erstellen, um eine ähnliche Liste von Tasks zu erledigen.
npms scripts
-Objekt ist in package.json
enthalten, was bedeutet, dass man keine neuen Dateien zu seinem Projekt hinzufügen muss. Das Objekt hat Eigenschaften, welches die Task-Namen sind, und Werte, welche die Befehle sind. Es ist so unglaublich einfach, dass es unfassbar ist, warum wir überhaupt jemals andere Build-Tools gebraucht haben. Nehmen wir unser an den Haaren herbeigezogenes JSHint-Beispiel und portieren es nach npm:
"devDependencies": {
"jshint": "latest",
},
"scripts": {
"lint": "jshint **.js"
}
Das ist alles! Du fügst eine weitere Abhängigkeit ein und eine Zeile Code für jedes Tool, das du verwenden möchtest. Dann rufst du einfach npm run lint
auf und voilà! Ein ungültiges Ergebnis wird einen non-zero-Exit-Code hervorbringen, und die Ergebnisse lassen sich streamen! Du kannst sogar Logs in Dateien umleiten. Zum Beispiel produzierst du einen CheckStyle-Bericht in JSHint einfach so:
"scripts": {
"lint": "jshint **.js --reporter checkstyle > checkstyle.xml"
}
Was aber, wenn du sowohl Checkstyle-Berichte (für CI) als auch Entwickler-Berichte haben willst? Es gibt keine Regeln für die Vergabe der Task-Bezeichnungen außer deinen eigenen Konventionen – also könnten wir eine Konvention von Grunt klauen und für unsere Tasks einen Namensraum mit :
festlegen, etwa:
"scripts": {
"lint": "jshint **.js",
"lint:checkstyle": "npm run lint -- --reporter checkstyle > checkstyle.xml"
}
In einen weiterführenden Artikel berichte ich detailliert über die Vor- und Nachteile von npm und seinen effektiven Einsatz und zeige, wie man eine erweiterbare Konfiguration, mehrfache Tasks, Streaming und mehr damit umsetzt.
Fazit
Mir ist klar, dass irgendjemand irgendwo einen sinnvollen Use-Case für Build-Tools wie Grunt und Gulp kennt. Aber ich glaube dennoch, dass npm 99 Prozent der Use-Cases elegant, mit weniger Code, weniger Aufwand und weniger Kosten als diese Tools meistern kann. Seit meiner Arbeit mit Grunt und Gulp und dem Versuch, meine Problem damit zu lösen, kann ich mit Sicherheit sagen, dass npm eine ausgezeichnete Alternative ist. Bei deinem nächsten Projekt rate ich dir, die Sache einfach zu halten – beginne mit npm als Build-Tool und wechsle nur, wenn du siehst, dass dein package.json
unhandlich wird. Ich denke, du wirst von den Ergebnissen positiv überrascht sein.
Du bist anderer Meinung? Total einverstanden? Hast Lust, über etwas anderes zu sprechen? Schick mir gern einen Tweet – auf Twitter heiße ich @keithamus.
Dieser Artikel erschien zuerst auf keithcirkel.co.uk.
Für einen 5er kann ich mal Korrektur lesen …
Dachte ich mir auch :)
Schon als ich nach den ersten Sätzen zwei verschiedene Schreibweisen für Brokkoli/Broccoli gelesen habe, hörte ich auf.
also wer grunt nur für jshint nutzt hat den Sinn von grunt nicht verstanden…
Da in diesem Artikel mehrfach der Vergleich zwischen jshint direkt über die Console und einem grunt-Script, in dem (scheinbar) nur jshint aufgerufen wird, auftaucht, scheint also der Author nur jshint einsetzen zu wollen (dann ist ein Tool wie grunt in der Tat nicht hilfreich) oder aber bewusst irreführende Vergleiche zu machen, denn schließlich liegt die Stärke der Tools darin, _mehrere_ Aufgaben zu lösen. Folglich so oder so ziemlich ungeschickt gelöst.
Wäre ja so, als wenn man einen Schwerlasttransporter mit einer Einkaufstasche vergleicht um sich ein Bröchen beim Bäcker zu kaufen….
@klaus: jshint war ja auch nur ein Beispiel des Autors. Er hat dazu auch eine Beispiel „package.json“-Datei veröffentlicht. Da wird auch „watch“ und „jade“ verwendet…
https://github.com/keithamus/npm-scripts-example/blob/master/package.json
Und damit hinkt der Vergleich von jshint vs. gulp/grunt jshint,
was ja mehrfach als Argument gegen die Tools in den Raum geworfen wurde:
„Natürlich muss man, um diese Grunt-Konfiguration zum Laufen zu kriegen, immer noch grunt jshint im Terminal ausführen, was auch nicht kürzer als jshint **.js ist.“
und
„[…] ich muss immer noch gulp jshint ausführen (was nur einen Tastendruck kürzer ist als jshint **.js“.
Wenn man schon dafür oder dagegen argumentiert, dann doch auch richtig :-)
Die Problematik ist (lt. Artikel) aber nicht die Länge der Eingabe in die Kommandozeile, sondern die zusätzlichen Abhangigkeiten, die bei jedem plugin mit installiert werden müssen und die Probleme, die bei einer neuen gulp/grunt Version auftreten können (zb: plugins sind nicht mehr kompatibel).
Ich finde dies Idee an sich ganz interessant, würde aber für zukünftige Projekte vermutlich doch weiterhin gulp verwenden.
Dass man hier nicht einfach mal einen kritischen Kommentar hinterlassen kann, ohne plakative Besserwisserei zu ernten…
Die Problematik der Abhängigkeiten habe ich mit keinem Wort bestritten (auch wenn man darüber durchaus auch diskutieren kann). Mir ging es einzig um die Tatsache, dass der Autor (vermutlich bis offensichtlich) einen seiner Kritikpunkte an den weit verbreiteten Build-Tools durch ein fiktives Beispiel untermauert, das gar keinen praktischen Nutzen hat und somit in meinen Augen vollkommen fehl am Platz ist.
Das kann man durchaus festhalten, ohne gleich den ganzen Artikel oder dessen Aussage per se zu besteiten – das ist jedenfalls meine Auffassung.
Ich habe mir vor 2 Monaten einen Gulp-Workflow erstellt, der für mich und mein Team als einheitliche Frontend-Entwicklungsumgebung aktuell recht gut funktioniert.
Dieser Umfasst:
– Sass-Compilierung inkl. Autoprefixer und Sourcemap
– JShint
– Jade-Compilierung
– Livereload
– http-server
Funktioniert alles wie gesagt ganz gut, allerdings stößt mir genau das auf, was der Author im Originalartikel auch anspricht: Für jedes Projekt müssen zig Plugins installiert werden, was auf Dauer doch extrem Speicher frisst.
Gerne würde ich auf eine npm-Lösung zurückgreifen, allerdings ist es nicht ganz so einfach, solch einen Workflow mit purem Node zu reproduzieren…
In meiner ehemaligen Firma hab ich eine Lösung entwickelt um „Grunt-Packages“ auf mehrere Projekte auszuspielen, statt die unzähligen Konfigurationen manuell zu pflegen:
https://github.com/redaxmedia/gsp
Statt in dem Gruntfile liegen die Konfigurationen in JSON Datein:
https://github.com/redaxmedia/gsp/tree/master/config
Von dem NPM Ansatz halte nicht viel, da es in der Realität komplexer Projekte komplett vorbei geht…
Bei allen Tools kommt es hauefig auch darauf an, wie man sie einsetzt. Man kann 1273 Plugins zu seinem Build-Tool hinzufuegen (egal ob Grunt, Gulp oder sonst was) oder man kann seine Konfiguration schlank halten. Leider werden diese Build-Tools haeufig als Allheilmittel fuer allerlei wirre Wuensche gesehen.
Auch ich wollte zuletzt fuer ein Projekt dazu uebergehen, NPM als Build-Tool zu benutzen. Dabei stellte sich jedoch heraus, dass die „watch“-Flag eines Node-Moduls nur eine Datei ueberwachen kann, grunt-contrib-watch, das ich gewohnt war, kann da aber wesentlich mehr. Somit musste ich wieder zu grunt greifen. Und hier stellt sich wieder die Frage: Nutze ich jetzt NPM UND Grunt? Damit habe ich auch wieder die Abhaengigkeit und kann doch gleich mit Grunt arbeiten?
Meiner Meinung nach existiert momentan noch kein vernuenftiger Workflow bzw gibt es auch zu viele verschiedene Anwendungsfaelle (Web-Apps, Websites, wird bspw. mit MongoDB oder PHP oder Java, oderoder gearbeitet). Man sollte als Entwickler offen sein neue Tools auszuprobieren und zu wissen, welches Tool fuer welchen Einsatzzweck gut zu sein scheint
Danke für den Artikel. Es wurden viele Kritikpunkte aufgeführt denen ich bisher nur „als Gefühl“ begegnet bin, die ich aber nicht konkret formulieren konnte. Eigentlich riecht alles sehr nach „make“, einem Tool das es lange gibt und auch „gut verstanden“ ist. Der „weiterführende Artikel“ ist ausgezeichnet und definitv lesenswert.