Entwicklung & Design

Statt Grunt, Gulp und Co.: Wie man npm als Build-Tool verwendet

Seite 2 / 2

Das Windows-Problem

Lass mich etwas klarstellen, bevor wir fortfahren. Weil npm zum Ausführen von Befehlen abhängig von der Shell des Betriebssystems ist, können Konfigurationen schnell inkompatibel werden. Während Linux, Solaris, BSD und Mac OSX mit Bash als vorinstallierter Shell kommen, tut Windows das nicht. Auf Windows greift npm für diese Dinge auf die Windows-Befehlskonsole zurück.

Offen gesagt ist das ein kleineres Problem, als es scheint. Ein gutes Stück Syntax, das in Bash funktioniert, wird auch in der Windows Befehlskonsole auf gleiche Weise funktionieren:

&& für die Verkettung von Tasks

& um Tasks gleichzeitig auszuführen

< um die Inhalte (stdin) einer Datei in einen Befehl einzufügen

> um den Output (stdout) eines Befehls weiterzuleiten und ihn in eine Datei einzufügen

| um den Output (stdout) eines Befehls weiterzuleiten und ihn an einen anderen Befehl zu senden

Die größten Probleme zwischen den beiden bestehen in der Verfügbarkeit und Benennung von Befehlen (z.B. ist cp in Windows COPY) und Variablen (Windows nutzt % für Variablen, Bash $). Ich habe das Gefühl, dass dies komplett umgehbare Probleme sind:

  1. Statt sich auf eingebaute Befehle zu verlassen, könntest du einfach Alternativen nutzen – zum Beispiel statt rm das npm rimraf package.
  2. Anstatt zu versuchen, Syntax zu verwenden, die nicht systemübergreifend kompatibel ist, bleib einfach bei den oben aufgeführten. Du wirst überrascht sein, wie viel du mit &&, >, | und < erreichen kannst. Variablen sind sowieso was für Anfänger.

Ersetzen von Build-Tools

OK, jetzt komme ich zur Sache. Wenn wir Build-Tools wie Grunt oder Gulp ersetzen wollen, benötigen wir vergleichbaren Ersatz für Plugins und Features dieser Tools. Ich habe mir die beliebtesten Tasks und Beispiele aus verschiedenen Projekten und Fragen von Kommentatoren meines letzten Artikels vorgenommen, und zeige, wie man sie in npm durchführt.

Verwenden mehrerer Dateien

Es gab ein paar Leute, die auf meinen letzten Artikel antworteten, dass der Vorteil von Task-Runnern ihre Fähigkeit ist, mit mehreren Dateien in Tasks umzugehen, indem sie Datei-„globs“ nutzen, die aussehen wie *.js, *.min.css oder assets/*/*. Dieses Feature wurde tatsächlich inspiriert von Bash, das wiederum vom glob-Befehl aus dem Unix von 1969 inspiriert war. Die Shell sieht automatisch auf Befehlszeilenargumente wie *.js und zieht die Sterne als Wildcards heraus. Die Verwendung von zwei Sternen erlaubt eine rekursive Suche. Wenn du auf einem Mac- oder Linuxrechner arbeitest, öffne mal die Shell und spiel damit herum (versuch so was wie ls *.js).

Nun, das Problem dabei liegt darin, dass die Windows-Befehlszeile diese Funktionalität nicht bietet. Zum Glück sendet Windows, wenn es ein Argument wie *.js bekommt, es wortwörtlich an die Applikation weiter, was heißt, dass Tool-Anbieter Kompatibilitäts-Bibliotheken installieren können, um Windows eine glob-artige Funktionalität zu verleihen. Viele, viele Tools in npm tun das. Die beiden populärsten glob-Libraries, minimatch und glob, sind Abhängigkeiten in 1500 Paketen, darunter JSHint, JSCS, Mocha, Jade, Stylus, Node-Sass … die Liste könnte man endlos weiterschreiben.

Das bedeutet, dass du in npm einfach Datei-globs verwenden kannst, etwa:

"devDependencies": {
"jshint": "latest"
},
"scripts": {
"lint": "jshint *.js"
}

Ausführen mehrerer Tasks

Grunt, Gulp und so weiter haben die Fähigkeit, verschiedene Tasks zusammenzuschnüren und daraus einen einzelnen Task zu machen – typischerweise ist das nützlich fürs Builden und Testen. Bei npm hast du hier zwei Möglichkeiten – je nachdem, welche semantisch besser passt. Du kannst entweder die pre- oder post-hooks verwenden, die sich gut eignen, wenn der eine Task den anderen voraussetzt (zum Beispiel, js zu konkatenieren, bevor man es minifiziert), oder du kannst den Bash-Operator && nutzen – etwa so:

"devDependencies": {
"jshint": "latest",
"stylus": "latest",
"browserify": "latest"
},
"scripts": {
"lint": "jshint **",
"build:css": "stylus assets/styles/main.styl > dist/main.css",
"build:js": "browserify assets/scripts/main.js > dist/main.js",
"build": "npm run build:css && npm run build:js",
"prebuild:js": "npm run lint"
}

Im obigen Beispiel führt build sowohl build:css als auch build:js aus – aber nicht, bevor es den lint-Task absolviert. Wenn man nach diesem Muster vorgeht, lassen sich auch die Tasks build:css oder build:js separat ausführen, und build:js wird vorher auch lint ausführen. Tasks können auf diese Weise beliebig zusammengesetzt und verkettet werden, und alles bleibt Windows-kompatibel.

Streamen zu verschiedenen Tasks

Eins von Gulps größten Features ist, dass es den Output nahtlos von einem Task an den nächsten streamt (im Gegensatz zu Grunt, das andauernd ins Dateisystem rein- und rausgeht). Bash und die Windows-Befehlszeile verfügen über den Pipe-Operator (|), der unseren Befehlsoutput (stdout) streamen und ihn an einen anderen Befehlsinput (stdin) weitersenden kann. Sagen wir mal, du willst dein gesamtes CSS erst durch Autoprefixer, dann durch CSSMin laufen lassen, um es dann in einer Datei auszugeben (durch Verwendung des >-Operators, der stdout in eine Datei ausgibt):

"devDependencies": {
"autoprefixer": "latest",
"cssmin": "latest"
},
"scripts": {
"build:css": "autoprefixer -b 'last 2 versions' < assets/styles/main.css | cssmin > dist/main.css"
}

Wie du siehst, fügt autoprefixer die CSS-vendor-Prefixe zu unserem CSS hinzu, welches dann zu cssmin weitergeleitet wird, welches den Output minifiziert – und dann wird das Ganze in das Verzeichnis dist/main.css geladen. Die meisten guten Tools werden stdin und stdout unterstützen, und der obige Code ist völlig kompatibel zu Windows, Mac und Linux.

Versions-Bumping

Versions-Bumping ist ein beliebter Grunt- oder Gulp-Task. Im Ergebnis erhöht es die Versionsnummer im package.json um eins, macht einen Git-Commit und taggt diesen Commit.

Das ist tatsächlich in npm eingebaut (es ist ja vor allem ein Paketmanager). Führe einfach npm version patch aus, um die Patch-Nummer zu erhöhen (zum Beispiel 1.1.1 -> 1.1.2), npm version minor für die mittlere Versionsnummer (zum Beispiel 1.1.1. -> 1.2.0) oder npm version major (zum Beispiel 1.1.1 -> 2.0.0). Es übernimmt die Übergabe und das Tagging des Pakets für dich, und alles, was dann noch zu tun bleibt, ist git push und npm publish.

Dies lässt sich auch komplett anpassen. Wenn du zum Beispiel nicht willst, dass es git tag ausführt, führ es einfach mit dem Flag --git-tag-version=false aus (oder schalte es dauerhaft ab mit npm config set git-tag-version false). Du willst die Commit-Message konfigurieren? Führ es einfach mit dem -m-Flag aus , zum Beispiel npm version patch -m „Bumped to %s“ oder lege das dauerhaft fest mit npm config set message „Bumped to %s“). Du kannst sogar die Tags für dich signen lassen, indem du es mit dem Flag --sign-git-tag=true ausführst (oder dies wiederum dauerhaft festlegen mit npm config set sign-git-tag true).

Clean

Viele Build-Runner verfügen über einen clean-Task. Dieser Task entfernt normalerweise einen Haufen von Dateien, damit du mit einer frischen Arbeitskopie einsteigen kannst. Nun, es stellt sich heraus, dass Bash von Haus aus einen ziemlich guten Clean-Befehl enthält: rm. Die Übergabe der -r (rekursiven) Flag lässt rm auch Verzeichnisse entfernen. Einfacher geht’s nicht:

"scripts": {
"clean": "rm -r dist/*"
}

Wenn du wirklich Windows-Support brauchst, welches rm nicht unterstützt: glücklicherweise gibt es rimraf, was ein cross-kompatibles Tool für die gleiche Aufgabe ist:

"devDependencies": {
"rimraf": "latest"
},
"scripts": {
"clean": "rimraf dist"
}

Dateien in eindeutige Namen kompilieren

Letztlich bedeutet das, die Funktionalität von gulp-hash und grunt-hash zu ersetzen – nimm einen Input aus JS und benenne ihn mit dem Hashwert der Inhalte. Das stellte sich als wirklich komplex heraus, wenn man es mit den bestehenden Kommandozeilen-Tools macht, also musste ich mir npm daraufhin anschauen, ob irgendetwas dafür passt, und die Antwort war Nein – also schrieb ich etwas (ich kann schon die Grunt-Gulp-Verfechter sagen hören, dass ich geschummelt habe). Ich habe zwei Anmerkungen dazu – erstens ist es ziemlich niederschmetternd, die Einzelkämpfer-Anstrengungen der verschiedenen Plugin-Autoren zu sehen – und keine generischen Lösungen, die mit jedem Build-Tool funktionieren. Zweitens – wenn du nichts Passendes finden kannst, schreib es selbst! Meine Hashmark-Library schlägt mit ungefähr genauso vielen Zeilen Code zu Buche wie die Grunt-/Gulp-Versionen und hat ein ähnliches oder besseres Feature-Set, abhängig vom Plugin – meins unterstützt sogar Streaming! Bei dem vorherigen Autoprefixer-Beispiel können wir unter Verwendung von Pipes eine Datei mit einem spezifischen Hash ausgeben:

"devDependencies": {
"autoprefixer": "latest",
"cssmin": "latest"
},
"scripts": {
"build:css": "autoprefixer -b '> 5%' < assets/styles/main.css | cssmin | hashmark -l 8 'dist/main.#.css'"
}

Ab jetzt ist der Output von build:css eine Datei in dist, benannt mit dem Hashwert, zum Beispiel dist/main.3ecfca12.css.

Watch

Das ist vermutlich der häufigste Grund, warum Leute Grunt oder Gulp verwenden, und bei weitem das meistgewünschte Beispiel in Kommentaren rund um meinen vorherigen Artikel. Ein Menge dieser Build-Tools enthält Befehle für das Überwachen eines Dateisystems, das Erkennen von Datei-Veränderungen (zum Beispiel beim Speichern) und dann für das Neuladen des Servers, Rekompilieren der Assets oder das erneute Ausführen von Tests. Sehr nützlich für schnelle Entwicklung. Es scheint, als würden die meisten Entwickler, die auf meinen letzten Post reagiert haben, davon ausgehen, dass es dafür außerhalb von Grunt/Gulp keine Möglichkeiten gäbe (oder vielleicht dachten, dass es zu kompliziert wäre, es ohne sie zu tun).

Also, die meisten Tools ermöglichen das von selbst – und sind normalerweise viel näher an den Details der Dateien, auf die man achten sollte. Mocha zum Beispiel hat die Option -w, ebenso Stylus, Node-Sass, Jade, Karma und andere. Du kannst diese Optionen so nutzen:

"devDependencies": {
"mocha": "latest",
"stylus": "latest"
},
"scripts": {
"test": "mocha test/",
"test:watch": "npm run test -- -w",

"css": "stylus assets/styles/main.styl > dist/main.css",
"css:watch": "npm run css -- -w"
}

Natürlich unterstützen das nicht alle Tools, und auch wenn sie es tun – du möchtest vielleicht mehrere Kompilierziele zu einem Task zusammenstellen, der Änderungen überwacht und darauf reagiert. Es gibt Werkzeuge, die Dateien überwachen und Befehle ausführen, wenn sich Dateien ändern, zum Beispiel watch, onchange, dirwatch oder sogar nodemon:

"devDependencies": {
"stylus": "latest",
"jade": "latest",
"browserify": "latest",
"watch": "latest",
},
"scripts": {
"build:js": "browserify assets/scripts/main.js > dist/main.js",
"build:css": "stylus assets/styles/main.styl > dist/main.css",
"build:html": "jade assets/html/index.jade > dist/index.html",
"build": "npm run build:js && npm run build:css && npm run build:html",
"build:watch": "watch 'npm run build' .",
}

Das wars – kurz und schmerzlos. Diese 13 Zeilen JSON überwachen dein ganzes Projektverzeichnis und bilden jedesmal, wenn sich eine Datei verändert, HTML-, CSS- und JS-Assets. Führ einfach npm run build:watch aus und fang an zu entwickeln! Du kannst es sogar mit einem kleinen Tool, welches ich geschrieben habe (wie gesagt, während ich diesen Artikel schrieb), noch weiter optimieren: Parallelshell, das für die gleichzeitige Ausführung mehrerer Prozesse sorgt – ein wenig wie hier:

"devDependencies": {
"stylus": "latest",
"jade": "latest",
"browserify": "latest",
"watch": "latest",
"parallelshell": "latest"
},
"scripts": {
"build:js": "browserify assets/scripts/main.js > dist/main.js",
"watch:js": "watch 'npm run build:js' assets/scripts/",
"build:css": "stylus assets/styles/main.styl > dist/main.css",
"watch:css": "watch 'npm run build:css' assets/styles/",
"build:html": "jade index.jade > dist/index.html",
"watch:html": "watch 'npm run build:html' assets/html",
"build": "npm run build:js && npm run build:css && npm run build:html",
"build:watch": "parallelshell 'npm run watch:js' 'npm run watch:css' 'npm run watch:html'",
}

Jetzt führt der Aufruf von npm run build:watch die individuellen Watcher alle durch Parallelshell aus, und wenn du zum Beispiel nur das CSS veränderst, dann wird nur das CSS noch einmal kompiliert. Wen du nur das JS veränderst, wird nur das JS neu kompiliert und so weiter. Parallelshell kombiniert die Outputs (stdout und stderr) aus jedem der Tasks und wartet auf den Exit-Code, um sicherzustellen, dass sich Logs und fehlgeschlagene Builds bemerkbar machen (nicht so wie der Bash/Windows &-Operator).

Live-Reload

Live-Reload war auch eine beliebte Sache. Wenn du nicht weißt, was Live-Reload ist – es ist eine Kombination aus Kommandozeilen-Tool und Browser-Erweiterung (oder eigenem Server) – wenn Dateien sich verändern, veranlasst Live-Reload die Seite, die du anschaust, sich neu zu laden, was bedeutet, dass du nie den Refresh-Button drücken musst. Das npm-Paket live-reload ist ein ziemlich brauchbarer Kommandozeilenclient dafür – es führt einen Server aus, der nur eine JS-Datei ausliefert, die, wenn du sie auf deiner Seite einbaust, die Seite über Änderungen benachrichtigt. Simpel, aber effektiv. Hier ist ein Beispiel, wie man das zum Laufen bringt:

"devDependencies": {
"live-reload": "latest",
},
"scripts": {
"livereload": "live-reload --port 9091 dist/",
}
<!-- In your HTML file -->
<script src="//localhost:9091"></script>

Wenn man npm run livereload ausführt, wird die HTML-Seite beim Aufruf auf den livereload-Server horchen. Jede Änderung an Dateien im dist/-Verzeichnis wird die Clients benachrichtigen und die Seite wird neu geladen.

Ausführung von Tasks, die keine Binaries enthalten

Ich wurde darauf aufmerksam gemacht, dass es Bibliotheken gibt, die keine Binaries enthalten – so wie favicon. Grunt-/Gulp-Plugins können somit nützlich sein, weil sie die Tools verpacken können, sodass sie innerhalb des Task-Runners zur Verfügung stehen. Wenn du ein Paket findest, welches du nutzen willst, das aber kein Binary hat, dann schreib einfach etwas JavaScript! Du müsstest es auch tun, wenn du Grunt oder Gulp verwendest, also hab keine Angst, ein wenig JavaScript irgendwo einzuspannen, das alles verdrahtet (oder sogar noch besser, schick ein PR an die Maintainer, in dem du sie davon überzeugst, eine Kommandozeilen-Oberfläche zu unterstützen!):

// scripts/favicon.js
var favicons = require('favicons');
var path = require('path');
favicons({
source: path.resolve('../assets/images/logo.png'),
dest: path.resolve('../dist/'),
});
"devDependencies": {
"favicons": "latest",
},
"scripts": {
"build:favicon": "node scripts/favicon.js",
}

Eine ziemlich komplexe Konfiguration

Viele haben mir gesagt, dass ich in meinem vorherigen Artikel das wichtigste über Task-Runner vergessen habe – sie sind dazu da, komplexe Sets von Tasks zusammenzuhalten, nicht nur fürs Ausführen von gelegentlichen Tasks. Also habe ich mir gedacht, dass ich dieses Stück abschließe mit einem komplexen Set von Tasks, die typisch für ein mehrere hundert Zeilen langes Gruntfile sind. Für dieses Beispiel möchte ich das Folgende tun:

  • Mein JS und lint nehmen, es testen und es in eine versionierte Datei kompilieren (mit einer separaten Sourcemap) und es nach S3 hochladen
  • Stylus nach CSS kompilieren, es zu einer einzelnen, versionierten Datei zusammenfassen (mit einer separaten Sourcemap), und es nach S3 hochladen
  • Watcher hinzufügen fürs Testen und Kompilieren
  • einen statischen Dateiserver hinzufügen, um meine Single-Page-App im Browser zu sehen
  • Livereload für CSS und JS hinzufügen
  • Einen Task anlegen, der all diese Dateien kombiniert, sodass ich nur einen Befehl eintippe und damit eine Umgebung aufbaue
  • Bonus-Punkt: automatisch ein Browser-Fenster öffnen, das meine Website anzeigt

Ich habe ein einfaches Repository auf GitHub angelegt namens npm-scripts-example. Es enthält das Layout für eine grundlegende Website und eine Package.json, welche die oben genannten Tasks einbaut. Die Zeilen, die dich vermutlich interessieren, sind:

  "scripts": {
"clean": "rimraf dist/*",

"prebuild": "npm run clean -s",
"build": "npm run build:scripts -s && npm run build:styles -s && npm run build:markup -s",
"build:scripts": "browserify -d assets/scripts/main.js -p [minifyify --compressPath . --map main.js.map --output dist/main.js.map] | hashmark -n dist/main.js -s -l 8 -m assets.json 'dist/{name}{hash}{ext}'",
"build:styles": "stylus assets/styles/main.styl -m -o dist/ && hashmark -s -l 8 -m assets.json dist/main.css 'dist/{name}{hash}{ext}'",
"build:markup": "jade assets/markup/index.jade --obj assets.json -o dist",

"test": "karma start --singleRun",

"watch": "parallelshell 'npm run watch:test -s' 'npm run watch:build -s'",
"watch:test": "karma start",
"watch:build": "nodemon -q -w assets/ --ext '.' --exec 'npm run build'",

"open:prod": "opener http://example.com",
"open:stage": "opener http://staging.example.internal",
"open:dev": "opener http://localhost:9090",

"deploy:prod": "s3-cli sync ./dist/ s3://example-com/prod-site/",
"deploy:stage": "s3-cli sync ./dist/ s3://example-com/stage-site/",

"serve": "http-server -p 9090 dist/",
"live-reload": "live-reload --port 9091 dist/",

"dev": "npm run open:dev -s & parallelshell 'npm run live-reload -s' 'npm run serve -s' 'npm run watch -s'"
}

(Wenn du dich wunderst, was das -s-Flag ist: Es ist nur dazu da, den Output von npm zu diesen Tasks stumm zu schalten, den Log-Output aufzuräumen – versuch mal, das zu deaktivieren und schau dir den Unterschied an.)

Um das gleiche in Grunt zu machen, würde ich ein Gruntfile von ein paar hundert Zeilen nehmen, plus (aus dem Bauch heraus geschätzt) um die zehn Extra-Abhängigkeiten. Es ist sicherlich subjektiv, welche Version besser lesbar ist – und während npm sicher nicht der heilige Gral der Lesbarkeit ist, denke ich persönlich, dass die npm-Skript-Direktive leichter zu durchschauen ist (zum Beispiel kann ich alle Tasks und was sie tun auf einen Blick sehen).

Fazit

Hoffentlich zeigt dieser Artikel, wie mächtig npm als Build-Tool sein kann. Hoffentlich hat er dir demonstriert, dass Tools wie Gulp und Grunt nicht immer das Erste sein müssen, mit dem man ein Projekt beginnt, und dass Tools, die du vermutlich schon auf deinem System hast, es wert sind, sie zu erkunden.

Wie immer kannst du gern mit mir auf Twitter diskutieren, ich bin @keithamus, du kannst mir auch dort folgen.

Dieser Artikel erschien zuerst auf blog.keithcirkel.co.uk.

Bitte beachte unsere Community-Richtlinien

Wir freuen uns über kontroverse Diskussionen, die gerne auch mal hitzig geführt werden dürfen. Beleidigende, grob anstößige, rassistische und strafrechtlich relevante Äußerungen und Beiträge tolerieren wir nicht. Bitte achte darauf, dass du keine Texte veröffentlichst, für die du keine ausdrückliche Erlaubnis des Urhebers hast. Ebenfalls nicht erlaubt ist der Missbrauch der Webangebote unter t3n.de als Werbeplattform. Die Nennung von Produktnamen, Herstellern, Dienstleistern und Websites ist nur dann zulässig, wenn damit nicht vorrangig der Zweck der Werbung verfolgt wird. Wir behalten uns vor, Beiträge, die diese Regeln verletzen, zu löschen und Accounts zeitweilig oder auf Dauer zu sperren.

Trotz all dieser notwendigen Regeln: Diskutiere kontrovers, sage anderen deine Meinung, trage mit weiterführenden Informationen zum Wissensaustausch bei, aber bleibe dabei fair und respektiere die Meinung anderer. Wir wünschen Dir viel Spaß mit den Webangeboten von t3n und freuen uns auf spannende Beiträge.

Dein t3n-Team

Ein Kommentar
Sascha Kühl
Sascha Kühl

Hallo …,

wer nicht affin mit dem ‚Werkzeug‘ (engl. Tool) resp. (Kommandozeilen-)Befehl ‚grep‘ u. / od. RegExp, also sog. ‚regulären Ausdrücken‘ (engl. regular expressions) wie (z. B.) BRE – basic regular expressions (einfache reguläre Ausdrücke), ERE – extended regular expressions (erweiterte reguläre Ausdrücke), PCRE – perl compatible regular expressions (perl-kompatible reguläre Ausdrücke) ist, der findet unter (https://goo.gl/ljr0pM) hierzu eine vollständige, deutschsprachige Anleitung u. Referenz.

Ciao, Sascha.

Antworten

Melde dich mit deinem t3n Account an oder fülle die unteren Felder aus.

Bitte schalte deinen Adblocker für t3n.de aus!

Hey du! Schön, dass du hier bist. 😊

Bitte schalte deinen Adblocker für t3n.de aus, um diesen Artikel zu lesen.

Wir sind ein unabhängiger Publisher mit einem Team bestehend aus 65 fantastischen Menschen, aber ohne riesigen Konzern im Rücken. Banner und ähnliche Werbemittel sind für unsere Finanzierung sehr wichtig.

Danke für deine Unterstützung.

Digitales High Five,
Stephan Dörner (Chefredakteur t3n.de) & das gesamte t3n-Team

Anleitung zur Deaktivierung