Build Scripts mit npm. Sind Grunt, Gulp und Bower obsolet?
Kurz und knapp: Grunt, Gulp und Bower sind nicht mehr nötig. Wie es allein mit npm funktioniert liest du Schritt für Schritt weiter unten.
Unsere Build Skripte haben wir bisher mit Grunt oder Gulp geschrieben und als Packet Manager haben wir Bower benutzt. Doch als die ersten Modulen den Support für Bower eingestellt haben, sind wir auf npm als Packet Manager für die Web Bibliotheken wie z.B. jQuery umgestiegen.
bower — ca. 50.000 Packages npm — ca. 240.000 Packages
Mit den npm-scripts ist es möglich die meisten Anwendungsfälle von Build Scripts zu realisieren. In diesem Artikel möchte ich am Beispiel unseres Frontend Boilerplates zeigen, wie die gewohnten Build Steps mit Grunt oder Gulp in npm umgesetzt werden können.
Vorteile von npm-scripts
- weniger Dependencies notwendig
- keine globalen Dependencies notwendig
- Skript Bereich in der package.json
- viel weniger Code
Nachteile
- keine Kommentare in der package.json möglich
- Outputs teilweise Unübersichtlich im Vergleich zu gulp und grunt
Wie erstelle ich ein Build Script mit npm?
Im folgenden versuche ich euch Schritt für Schritt typische Tasks aus grunt und gulp in npm-scripts zu übersetzen und zu erläutern. Als Beispiel nehmen wir das Boilerplate Projekt, welches wir intern nutzen.
Das Boilerplate beinhaltet eine SASS Pipeline, JS Bundles für jQuery / Foundation und React sowie den Panini Compiler von Zurb für HTML Layouts und Partials. Für die komfortable Entwicklung benutzen wir LiveReload und einen Watcher auf alle Dateien.
Die Verzeichnisstruktur der Boilerplate:
/src
/src/html (HTML Dateien)
/src/layouts (Panini Layouts)
/src/partials (Panini Partials)
/src/js/main.js (Einstiegspunkt für jQuery und Foundation)
/src/react/react.js (Einstiegspunkt für React)
/src/scss/style.scss (Einstiegspunkt für SCSS) /scripts/ (NodeJS Build Scripts) /app (Zielverzeichnis)
/app/css (CSS Output
/app/js (JS Bundle Output)
/app/*.html (Generierte HTML Dateien)
Das Zielverzeichnis app/
wird vor dem Build aufgeräumt.
"clean": "rimraf app/"
Mit dem Prefix pre- und post- können Skripte angegeben werden, die explizit vor oder nach den genannten Skript ausgeführt werden. In unserem Beispiel soll clean
vor jedem build
ausgeführt werden, daher fügen wir prebuild
als auszuführenden Task hinzu.
"prebuild": "npm run clean"
SCSS Pipeline
"build:css": "node-sass src/scss/style.scss app/css/main.css"
Wir nutzen node-sass um die scss Dateien zu kompilieren. Abhängigkeiten zu anderen scss Dateien oder Frameworks wie in unserem Beispiel zum Foundation Framework können in der style.scss angegeben werden:
@import "../../node_modules/foundation-sites/scss/foundation";
JS Bundles
Wir stellen zwei verschiedene Bundles bereit, eins für Standard JavaScript und den Foundation Funktionen. Das andere für die React Komponenten.
"build:js": "mkdirp app/js && browserify src/js/main.js -o app/js/main.js"
"build:react": "mkdirp app/js && browserify -t babelify src/react/react.js -o app/js/react.js"
mkdirp nutzen wir um das Zielverzeichnis zu erstellen, falls es noch nicht verfügbar ist. Browserify wird für das Generieren des Bundles verwendet. Für die Verwendung von Foundation und jQuery sind folgende Zeilen in die main.js einzufügen:
global.$ = global.jQuery = require('jquery');
require('foundation-sites'); $( document ).ready(function() { $(document).foundation();
});
Damit browserify die React Dateien übersetzen kann, muss eine Datei .babelrc
existieren, die folgende Konfiguration Zeile enthält:
{"presets": ["react"] }
HTML Layouts mit Panini
"build:html": "node ./scripts/panini.js"
Für Layout Dateien und HTML Fragmente wie z.B. Footer nutzen wir Panini, das auf der Handlebars Engine basiert. Es ist ein sehr schlankes Tool, das Layouts, Partials und weitere nützliche Funktionen mitbringt.
An diesem Beispiel kann man auch sehen, wie ein Node JS Script als Build Script ausgeführt werden kann, wenn das Package keinen CLI Befehl mitbringt. Die panini.js besteht aus wenigen Zeilen:
var src = require('vinyl-fs').src;
var dest = require('vinyl-fs').dest;
var panini = require("panini"); panini.refresh(); src('src/html/**/*.html')
.pipe(panini({
root: 'src/html',
layouts: 'src/layouts/',
partials: 'src/partials/'
}))
.pipe(dest('./app'));
Build the webpage
"build": "npm run build:css & npm run build:html & npm run build:js & npm run build:react"
Der build
Task, der alle Einzelaufgaben vereint führt die jeweiligen Skripte aus. Wichtig ist hierbei, dass vorher der prebuild
Task ausgeführt wird.
Das Dateisystem überwachen
"watch:html": "nodemon -q -w src/ -e html --exec npm run build:html"
"watch:js": "nodemon -q -w src/js -e js --exec npm run build:js"
"watch:react": "nodemon -q -w src/react -e js --exec npm run build:react"
"watch:css": "nodemon -q -w src/scss -e scss --exec npm run build:css"
Wir überwachen jeweils die Dateien, die den einzelnen Build Parts angehören, damit nur der einzelne Build Task zur Aktualisierung ausgeführt werden muss. Das Dateisystem überwachen wir mit nodemon, welches sich dafür sehr gut eignet.
Lokaler Entwicklungsserver mit Live-Reload
"serve": "live-server --port=8080 app/"
Auch das gibt es integriert und voll automatisiert in einem npm Modul. Es wird hier ein Server im Root app/ auf dem Port 8080 gestartet, der automatisch bei jeder Dateiänderungen ein Reload ausführt und beim ersten Ausführen den Browser mit entsprechender Seite startet.
Das komplette Build Skript
"scripts": {
"clean": "rimraf app/",
"prebuild": "npm run clean",
"build:css": "node-sass src/scss/style.scss app/css/main.css",
"build:js": "mkdirp app/js && browserify src/js/main.js -o app/js/main.js",
"build:html": "node ./scripts/panini.js",
"build:react": "mkdirp app/js && browserify -t babelify src/react/react.js -o app/js/react.js",
"build": "npm run build:css & npm run build:html & npm run build:js & npm run build:react",
"watch:html": "nodemon -q -w src/ -e html --exec 'npm run build:html'",
"watch:js": "nodemon -q -w src/js -e js --exec 'npm run build:js'",
"watch:react": "nodemon -q -w src/react -e js --exec 'npm run build:react'",
"watch:css": "nodemon -q -w src/scss -e scss --exec 'npm run build:css'",
"serve": "live-server --port=8080 app/",
"dev": "npm run build && parallelshell 'npm run watch:js' 'npm run watch:react' 'npm run watch:html' 'npm run watch:css' 'npm run serve'"
}
Vergleich: unser gulp Skript mit exakt gleicher Funktionalität war 140 Zeilen lang.
Mit dem letzten Skript npm run dev wird nun das Projekt gebaut, die Website im Browser lokal aufgerufen und das Dateisystem auf Änderungen überwacht und mittels LiveReload direkt im Browser aktualisiert.
Es wird hier parallelshell genutzt, damit die Watch Prozesse parallel ausgeführt werden können und erst beendet werden, sobald der letzte Task per Ctrl-C
beendet wird.
Zusammenfassung
npm-scripts reichen für die meisten Anwendungsfälle aus und sind viel schlanker als entsprechende gulp oder grunt Skripte. Dennoch sollte man nicht zwangsläufig bei bestehenden Projekten darauf umsteigen, wenn das Team in den momentanen Build Skripten eingearbeitet ist. Denn man muss stets darauf achten, dass alle beteiligten Entwickler den Build Prozess verstehen.