Statt Grunt, Gulp und Co.: Wie man npm als Build-Tool verwendet
Letzten Monat habe ich meine Meinung dazu geäußert, warum wir aufhören sollten, Grunt, Gulp und so weiter zu verwenden. Ich schlug vor, dass wir statt dessen anfangen sollten, npm zu verwenden. npms scripts
-Direktive kann alles, was diese Build-Tools können, kürzer, eleganter, mit weniger Paketabhängigkeiten und weniger Verwaltungs-Overhead übernehmen. Der erste Entwurf des Original-Artikels war weit über 6.000 Worte lang – weil er in die Tiefe ging darüber, wie man npm als Alternative benutzen könnte, aber ich löschte ihn zuliebe eines kürzeren – und weil es ein Artikel war, in dem ich meine Meinung ausdrückte, kein Tutorial. Dennoch war die Antwort ziemlich überwältigend – viele Leute antworteten darauf, dass diese Build-Tools ihnen Features böten, die npm nicht kann (oder nicht macht), und einige Entwickler waren so dreist, mir ein Gruntfile zu übergeben mit den Worten: „Wie könnte man das in npm machen?!“ Deshalb dachte ich mir, ich ziehe die How-tos aus dem Original-Entwurf heraus und erstelle einen neuen Artikel, der sich nur darauf konzentriert, wie man diese üblichen Tasks mit npm erledigt.
npm ist ein fantastisches Werkzeug, das viel mehr bietet, als man auf den ersten Blick sieht. Es ist zum Backbone der Node.js-Community geworden – viele, auch ich, nutzen es jeden Tag sehr ausgiebig. Wenn man meine Bash-History anschaut (na gut, Fish-History), dann ist npm
tatsächlich nach git
mein meistgenutzter Befehl. Dennoch finde ich jeden Tag neue Features von npm (und natürlich werden immer noch neue entwickelt!). Die meisten von ihnen sind dafür gedacht, npm zu einem großartigen Paketmanager zu machen, aber npm hat ein tolles Set an Funktionen, die dem Ausführen von Tasks in einem Paket-Lebenszyklus gewidmet sind – mit anderen Worten: es ist ein tolles Tool für Build-Skripte.
npm-Skripte
Zuerst einmal müssen wir herausfinden, wie npm unsere Build-Skripte managen kann. Als Teil von npms Core verfügt es über den npm run-script
-Befehl (kurz npm run
). Dieser Befehl durchsucht dein package.json
und zieht das scripts
-Objekt heraus. Das erste Argument, das an npm run
geht, bezieht sich auf eine Eigenschaft im scripts
-Objekt – es führt den Wert der Eigenschaft als Befehl in der Standard-Shell des Betriebssystems aus (normalerweise Bash, außer auf Windows – aber dazu später). Sagen wir also, du hast eine package.json
Konfiguration, die wie folgt aussieht:
{
"name": "myproject",
"devDependencies": {
"jshint": "latest",
"browserify": "latest",
"mocha": "latest"
},
"scripts": {
"lint": "jshint **.js",
"test": "mocha test/"
}
}
Wenn du npm run lint
ausführst, wird npm eine Shell öffnen und jshint **.js
ausführen. Wenn du npm run test
aufrufst, wird npm eine Shell öffnen und mocha test/
ausführen. Die Shell-Umgebung hat deinen node_modules/.bin
-Ordner zum Pfad
hinzugefügt, was bedeutet, dass jede Abhängigkeit, die Binaries installiert, direkt ausführbar wird – in anderen Worten, man spart sich das "./node_modules/.bin/jshint **.js"
oder "$(npm bin)/jshint **.js"
. Wenn du npm run ohne Argumente ausführst, gibt es dir eine Liste der verfügbaren Befehle zurück, zum Beispiel:
Available scripts in the user-service package:
lint
jshint **.js
test
mocha test/
Die npm run
-Shell-Umgebung bietet eine Menge hilfreiche Features, um sicherzustellen, dass deine Skripte so kurz wie möglich werden. Zum Beispiel enthält der Shell-Pfad
deinen ./node_modules/.bin/
-Ordner, was bedeutet, dass alle installierten Abhängigkeiten, welche Binaries enthalten, direkt aus der Script-Shell aufgerufen werden können. Außerdem gibt es eine ganze Menge super-praktische Umgebungsvariablen, die npm anzeigt, wie zum Beispiel den derzeit laufenden Task, den Paketnamen und die Version, das npm-Loglevel und so weiter. Du kannst sie alle entdecken, wenn du ein Script erstellst und aufrufst, das env
ausführt:
"scripts": {
"env": "env"
}
Shortcut-Skripte
npm bietet auch ein paar bequeme Shortcuts. Die Befehle npm test
, npm start
und npm stop
sind alle Shortcuts für ihre run-Äquivalente. So ist npm test
zum Beispiel nur ein Shortcut für npm run test
. Diese Shortcuts sind aus zweierlei Gründen nützlich:
1. Es gibt beliebte Tasks, welche in den meisten Projekten Verwendung finden, und daher ist es nett, nicht jedes Mal so viel tippen zu müssen.
2. Viel wichtiger – es bietet ein Standard-Interface innerhalb von npm fürs Testen, Starten und Stoppen von Paketen. Viele CI-Tools wie etwa Travis nutzen dieses Verhalten und machen das Default-Kommando für ein Node.js-Projekt zu npm test
. Es wird dadurch auch einfacher, neue Entwickler zu deinem Projekt dazuzuholen, wenn sie wissen, dass sie einfach ein Skript wie npm test
ausführen können, ohne jemals irgendwelche Dokumentation lesen zu müssen.
Pre- und Post-Hooks
Ein weiteres cooles Feature von npm ist, dass jedes ausführbare Skript auch über ein Set von pre-
und post-
hooks verfügt, welche sich ganz einfach im scripts
-Objekt definieren lassen. Wenn du zum Beispiel npm run lint
ausführst, wird npm, obwohl es keine vorgefasste Idee davon hat, was der lint
-Task ist, sofort npm run prelint
ausführen, gefolgt von npm run lint
, gefolgt von npm run postlint
. Das gleiche gilt für jeden Befehl, inklusive npm test
(npm run pretest
, npm run test
, npm run posttest
). Die pre- und post-Skripte sind außerdem Exit-Code-sensitiv. Wenn also dein pretest
-Skript mit einem non-zero-Exit-Code endet, hält npm sofort an und führt die Skripte test
und posttest
nicht aus. Du kannst aber ein pre-
Skript nicht mit pre
versehen, also wird prepretest
ignoriert. npm führt auch die pre-
und post-
hooks für ein paar interne Befehle aus: install
, uninstall
, publish
, update
. Du kannst diese Verhalten für die internen Befehle nicht überschreiben – aber du kannst ihr Verhalten mit pre-
und post-
Skripts beeinflussen. Das heißt, dass du Sachen tun kannst wie das hier:
"scripts": {
"lint": "jshint **.js",
"build": "browserify index.js > myproject.min.js",
"test": "mocha test/",
"prepublish": "npm run build # also runs npm run prebuild",
"prebuild": "npm run test # also runs npm run pretest",
"pretest": "npm run lint"
}
Weitergeben von Argumenten
Ein anderes cooles Feature von npm (mindestens seit npm 2.0.0) ist das Durchreichen von Argumenten-Sets an die untergeordneten Tools. Dies kann ein wenig kompliziert sein, aber hier ist ein Beispiel:
"scripts": {
"test": "mocha test/",
"test:xunit": "npm run test -- --reporter xunit"
}
Mit dieser Konfiguration können wir ganz einfach npm run test
ausführen – was mocha test/
ausführt, aber wir können es mit benutzerdefinierten Parametern mit einem --
Präfix erweitern. Zum Beispiel führt npm run test -- anothertest.js
mocha/ anothertest.js
aus, oder, noch nützlicher, expandiert npm run test -- --grep parser
nach mocha test/ --grep parser
(das nur die Tests mit „parser“ im Titel ausführt). Im package.json enthalten ist test:xunit
, was erfolgreich mocha test --reporter xunit
ausführt. Dieses Setup kann unglaublich nützlich sein für das Zusammenstellen von Befehlen für einige fortgeschrittene Konfigurationen.
npm-Konfigurationsvariablen
Eine Sache noch, die sich zu erwähnen lohnt – npm besitzt eine config
-Direktive für deine package.json
. Das ermöglicht dir das Setzen von beliebigen Variablen, welche als Umgebungsvariablen in deinen scripts
aufgegriffen werden können. Hier ist ein Beispiel:
"name": "fooproject",
"config": {
"reporter": "xunit"
},
"scripts": {
"test": "mocha test/ --reporter $npm_package_config_reporter"
"test:dev": "npm run test --fooproject:reporter=spec"
}
Hier hat das config
-Objekt die Eigenschaft reporter
auf 'xunit'
gesetzt. Alle Konfigurationsoptionen werden als Umgebungsvariablen zugänglich, indem sie das Präfix npm_package_config_
erhalten (was zugegebenermaßen die Variablennamen ganz schön aufbläht). In dem obigen Beispiel nutzt der Befehl npm run test
die Variable $npm_package_config_mocha_reporter
, die zu mocha test/ --reporter xunit
erweitert wird. Diese kann auf zwei bequeme Arten ersetzt werden:
- Genauso wie der
test:dev-
Task kannst du die Konfigurationsvariable reporter zuspec
ändern, indem du--fooproject:reporter
verwendest. Du kannstfooproject
durch deinen Projektnamen ersetzen undreporter
mit der Konfigurations-Variable, die du überschreibst. - Sie können auch als Teil der Benutzerkonfiguration überschrieben werden. Durch das Ausführen von
npm config set fooproject:reporter spec
erscheint ein Eintrag in meinem~/.npmrc
(fooproject:reporter=spec
), der zur Laufzeit gelesen wird und die Variablenpm_package_config_reporter
überschreibt. Das bedeutet, dass auf meinem lokalen Rechner für alle Zeitennpm run test
nachmocha test/ --reporter spec
erweitert wird. Ich kann meine persönlichen Einstellungen dafür mitnpm config delete fooproject:mocha_reporter
entfernen. Ein gutes Setup dafür ist das Vorhalten von einigen vernünftigen Defaults in deinerpackage.json
– aber deine eigenen Anpassungen können in deinem~/.npmrc
verstaut werden.
Okay, um ehrlich zu sein, bin ich nicht gerade verliebt in die Art, wie das funktioniert. Während das Setup trivial erscheint (ein „config“
-Objekt in deiner JSON vorzuhalten), erscheint der Versuch, diese zu benutzen, zu langatmig und kompliziert. Ich hätte auch gern einen einfacheren Weg, die package-Konfigurationen zu überschreiben, ohne den Package-Namen angeben zu müssen – es wäre toll, wenn es statt dessen Standard-Konventionen gäbe, in denen ich meine bevorzugten mocha reporter für alle Packages in meinem ./.npmrc
festlegen könnte.
Der andere Nachteil dieser Konfigurationen ist, dass sie nicht besonders Windows-freundlich sind – Windows verwendet %
für Variablen, während bash $
verwendet. Sie funktionieren ohne Probleme, wenn du sie innerhalb eines Node.js-Skripts nutzt, aber wenn jemand einen Weg kennt, wie man sie in Windows über die Shell-Befehle zum Laufen bringt, lasst es mich wissen!
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:
- Statt sich auf eingebaute Befehle zu verlassen, könntest du einfach Alternativen nutzen – zum Beispiel statt
rm
das npm rimraf package. - 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.
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.