Skip to content

Latest commit

 

History

History

nodejs+npm

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 

Umgang mit Node.js + NPM

Grundwissen

Node.js ist eine server-seitige Plattform und die Laufzeitumgebung für JavaScript. Damit kann der server-seitige JavaScript Code ausgeführt werden. Für die Installation wird eine Long Term Support (LTS) Version empfohlen, aber es kann auch jede beliebige releaste Version installiert werden.

Mit Node.js wird auch Node Package Manager (Paketmanager) npm automatisch installiert. Mit dem Paketmanager lassen sich Node.js Module (noch Packages genannt) aus einem npm Registry installieren. Der Pfad zu Node.js und npm wird zur Umgebungsvariable PATH hinzugefügt (normalerweise automatisch, falls man nichts anderes bei der Installation ausgewählt hat). Danach kann man npm von überall in der Console aufrufen. Die installierten Node.js und npm Versionen lassen sich wie folgt abfragen:

node -v
npm -v

Eine bereits installierte Node.js Version lässt sich wie folgt aktualisieren:

npm cache clean -f
npm install -g n
n stable

Anstatt von stable kann eine Version auch explizit angegeben werden, z.B.

n 5.5.0

Auf Unix und Mac basierenden Betriebssystemen müssen die obigen Befehle mit sudo (mit Admin-Rechten) ausgeführt werden. Ein beliebiges Modul (noch Paket genannt) kann mit Hilfe von npm wie folgt installiert werden:

npm install <modulename>

Beispiel:

npm install gulp

npm hat Tausende von Modulen. Sucht man ein bestimmtes Modul, hat man zwei Moglichkeiten - die Suche auf der npm Homepage oder via

npm search <search term>

Projektübergreifende Tools werden global mit dem Parameter -g oder --global installiert. Beispiel:

npm install gulp -g

npm selbst kann man nachträglich wie folgt aktualisieren:

npm install npm -g

Generell werden Module mit

npm update <modulename>

aktualisiert. Der Befehl muss im Projekt-Hauptverzeichnis ausgeführt werden. Für globale Module wird der Parameter -g oder --global benutzt. Wird der Modulename weggelassen, werden alle Module in einem Rutsch aktualisiert. Natürlich lassen sich Module auch deinstallieren. Dafür schreibt man uninstall an der Stelle install. Z.B.

npm uninstall gulp -g
npm uninstall webpack

Datei package.json und Projektabhängigkeiten

Wo genau werden aber die projektspezifischen Module installiert? Dazu legen wir ein leeres Projekt-Verzeichnis an und führen dort folgendes aus:

npm init

Daraufhin wird eine Datei namens package.json angelegt. Das Erzeugen von package.json ist interaktiv. Es werden einige Fragen gestellt, die man beantworten muss. Man kann die Datei selbstverständlich auch nachträglich anpassen, z.B. die Angaben für repository, keywords, script, usw. hinzufügen. Eine gute Übersicht aller möglichen Einstellungen gibt es in der offizellen Dokumentation. Wenn man die Applikation nicht in dem öffentlichen npm Repository veröffentlichen will, sollte man die Property private auf true setzen: "private": true.

Java-Entwickler können diese Datei als pom.xml in Maven vorstellen. Dort werden auch Projekt-Dependencies verwaltet. Es gibt zwei Arten von Dependencies:

1.) Dependencies, die man zur Laufzeit braucht. Die werden mit der Web Applikation gebündelt und ausgeliefert. Das wären z.B. jQuery, AngularJS, TypeScript o.ä. Bibliotheken. Sie werden im Projekt-Hauptverzeichnis installiert, d.h. lokal, ohne den Parameter -g oder --global. Java-Entwickler können sich solche Dependencies als JAR Dateien unter WEB-INF/lib vorstellen. Die Module werden in einen Unterordner namens node_modules heruntergeladen und stehen damit dem ganzen Projekt zur Verfügung. Führt man z.B. npm install underscore aus, hat man die folgende Struktur:

<projekt root>
    node_modules
        underscore
            package.json
            underscore.js
            README.md
            ...

Installiert man eine Dependency mit

npm install <modulename> --save

wird sie auch in der package.json Datei, in der Sektion dependencies, gespeichert. D.h. in der package.json Datei werden dann alle benötigten Dependencies aufgelistet. Das ist wichtig, wenn man in einem Team arbeitet. package.json steht unter Versionskontrolle. Wenn nun eine andere Person das Projekt auscheckt und npm install ausfrühren läßt, hat sie automatisch alle Dependencies bei sich lokal im Verzeichnis node_modules (node_modules steht nicht unter Versionskontrolle).

2.) Dependencies, die man zur Build-Zeit braucht. Das sind z.B. die Build-Tools wie Gulp oder Webpack, Development-Server, Test-Frameworks, Linting Tools, u.ä. Java-Entwickler können sich solche Dependencies als Maven Plugins vorstellen. Sie werden nicht mit der Web Applikation ausgeliefert. Solche Dependencies muss man mit

npm install <modulename> --save-dev

installieren. Z.B. npm install webpack --save-dev. Damit werden sie auch in der package.json Datei, in der Sektion devDependencies, gespeichert. Jeder im Team kann sie nutzen, nachdem er npm install ausgeführt hat.

Die Datei package.json hat somit zwei wichtige Sektionen: devDependencies und dependencies. Ein Auschnitt aus der package.json könnte so aussehen:

"devDependencies": {
  "css-loader": "^0.23.1",
  "rimraf": "^2.5.2",
  "style-loader": "^0.13.1",
  "webpack": "^1.13.0"
},
"dependencies": {
  "promise-light": "^0.1.8"
  "jquery": ">=1.11.0"
}

Es gibt noch eine Sektion peerDependencies. Dort werden alle Dependencies mit bestimmten Versionen aufgelistet, die man braucht, damit das aktuelle Modul (eine Bibliothek) normal funktionieren kann. Die npm Version 3 gibt eine Warnung aus, falls eine Dependency aus peerDependencies noch nicht installiert ist. Frühere Versionen von PrimeNG haben z.B. eine Dependency zu PrimeUI verlangt:

"peerDependencies": {
  "primeui": "^4.1.12"
}

Semantische Versionierung mit Best Practice

Node.js Module verfolgen eine so genannte semantische Versionierung. Ein Modul hat eine Version im Format X.Y.Z (Major.Minor.Patch). Behebt der Entwickler einen Fehler, ohne die Abwärtskompatibilität zu beeinträchtigen, wird lediglich die Patchnummer um 1 erhöht. Fügt der Entwickler neue Funktionalität hinzu ohne Beeinträchtigung der Abwärtskompatibilität, wird die Minornummer um 1 erhöht. Beeinträchtigt der Entwickler die Abwärtskompatibilität, wird die Majornummer um 1 erhöht. Bei den Dependencies kann kann nicht nur exakte Versionen angeben, sondern auch Ranges und mehr. npm semver calculator stellt eine interaktive "Spielwiese" dar, um dies auszuprobieren. Es wird schnell klar, wie man Major, Minor, Patch Versionen, Ranges oder exakte Versionen auswählen kann. Hier sind einige Beispiele:

"dependencies": {
  "package1": "1.0.0",         // exakte 1.0.0 Version
  "package2": "1.0.x",         // nur die Patch-Releases in Version 1.0
  "package3": "*",             // letzte Version (nicht empfohlen)
  "package4": ">=1.0.0",       // beliebige Änderungen nach 1.0.0
  "package5": "<1.9.0",        // eine Version kleiner als 1.9.0
  "package6": "~1.8.0",        // Shorthand für >= 1.8.0 < 1.9.0
  "package7": "^1.1.0",        // Shorthand für >=1.1.0 < 2.0.0
  "package8": "latest",        // letzte Version
  "package9": "<1.0.0 || >=2.3.1 <2.4.5 || >=2.5.2 <3.0.0"
}

Die exakten Versionen und Ranges kann man auch explizit bei der Installation angeben. Auch Tags (Aliases für Versionen) und bestimmte Git-Branches lassen sich angeben. Beispiele:

npm install jquery@1.11.0 --save
npm install sax@">=0.1.0 <0.2.0" --save
npm install typescript@beta --save
npm install -g git+https://git@github.com/gulpjs/gulp-cli.git#4.0

Die folgenden Befehle

npm view <modulename> versions

und

npm view <modulename> dist-tags

geben alle verfügbaren Versionen bzw. Tags eines Modules aus. Z.B.

npm view typescript dist-tags

{ latest: '1.8.10', next: '2.1.0-dev.20160814', beta: '2.0.0' }

npm hat eine eingebaute Versions-Bumping. Damit wird die Versionsnummer in der package.json Datei um eins erhöht, ein Git-Commit gemacht und dieser Commit getaggt. Hier sind die Befehle:

npm version patch  // Beispiel-Ergebnis: 1.1.1 -> 1.1.2
npm version minor  // Beispiel-Ergebnis: 1.1.1 -> 1.2.0
npm version major  // Beispiel-Ergebnis: 1.1.1 -> 2.0.0

Mit dem Parameter --git-tag-version=false kann man das Tagging unterbinden. Das geht auch dauerhaft mit dem Befehl npm config set git-tag-version false. Mit dem Befehl npm config set kann man übrigens verschiedene Konfigurationsmöglichkeiten vornehmen. Es wird oft verwendet, um einen Proxy-Server in einem Unternehmensnetz für npm bekanntzugeben.

npm config set proxy http://proxy.company.com:8080
npm config set https-proxy http://proxy.company.com:8080

oder wenn man einen privaten npm Server für das Unternehmen aufgesetzt hat (anstatt Default https://registry.npmjs.org/) und ihn beim npm registrieren möchte.

npm config set registry https://<whatever>/

Generell, mit npm config set <whatever> wird die Datei .npmrc modifiziert. Die Datei .npmrc wird entweder im Projekt-Hauptverzeichnis (Konfiguration pro Projekt) oder im Benutzer-Homeverzeichnis angelegt. D.h. entweder irgendwo in /path/to/my/project/.npmrc oder ~/.npmrc.

Oft wird es empfohlen, nur die Patch-Releases aktualisieren zu lassen. D.h. nur die Patchnummern sind variabel (beispielweise 1.0.x). Damit werden böse Überraschungen vermieden, dass das Projekt plötzlich nicht gebaut werden kann. Das Problem liegt oft in den transitiven Abhängigkeiten. Angenommen, Ihr Modul A hängt vom Modul B ab und das Modul B hängt seinerseits vom Modul C ab. Angenommen, die Module B und C sind third-party Module, d.h. Sie haben keinen Einfluss darauf. Wird jetzt die Version des Modules C geändert, kann der Build u.U. fehlschlagen, wenn sogar die Versionen der Modulen A und B nicht geändert wurden. Man kann die Gefahr eines fehlgeschlagenes Builds noch weiter minimieren, indem man shrinkwrap verwendet. Der Befehl

npm shrinkwrap

erlaubt die Versionen aller im Projekt benutzen Modulen mit ihren Abhängigkeiten unter node_modules "einzufrieren". Damit werden die Versionsänderungen sozusagen "gesperrt". Wie funktioniert das? Der Befehl npm shrinkwrap erzeugt die Datei npm-shrinkwrap.json, in der die exakten Versionen aller fürs Projekt installierten Modulen mit ihren Abhängigkeiten aufgelistet sind. Die Datei npm-shrinkwrap.json wird unter Versionskontrolle gestellt. Nun bekommen alle Teamkollegen genau die gleichen exakten Versionen aller Modulen mit ihren Abhängigkeiten, nachdem sie npm install ausgeführt haben.

Weitere nützliche NPM-Befehle

Weitere nützliche npm Befehle sind ls, outdated und link. Mit ls werden alle installierten lokalen oder globalen Module aufgelistet. Man kann dazu noch das Flag -l für die Ausgabe der kurzen Beschreibungen nutzen. Schreibt man --depth=0, werden nur noch die Top-Level Module und nicht der ganze Dependency-Baum aufgelistet.

npm ls -l --depth=0
npm ls -l --depth=0 -g

Die Ausgabe sieht ungefähr so aus:

+-- connect@3.4.1
|   High performance middleware framework
|   git+https://github.com/senchalabs/connect.git
|   https://github.com/senchalabs/connect#readme
+-- del@2.2.0
|   Delete files/folders using globs
|   git+https://github.com/sindresorhus/del.git
|   https://github.com/sindresorhus/del
+-- gulp-clean-css@2.0.7
|   Minify css with clean-css.
|   git+https://github.com/scniro/gulp-clean-css.git
|   https://github.com/scniro/gulp-clean-css#readme
+-- gulp-concat@2.6.0
|   Concatenates files
|   git+https://github.com/wearefractal/gulp-concat.git
|   https://github.com/wearefractal/gulp-concat#readme

Manche Module werden mit extraneous als irrelevant bzw. überflüssig angezeigt. Solche Module kann man mit dem Befehl npm prune aufräumen. Beim Aufruf von npm prune gleicht npm die Liste der installierten Module mit der Liste der Abhängigkeiten in der Datei package.json ab und entfernt alle Abhängigkeiten, die von package.json nicht explizit gefordert sind.

Mit outdated wird geprüft welche lokale bzw. globale Module aktualisiert werden können.

npm outdated 
npm outdated -g --depth=0

Die Ausgabe sieht ungefähr so aus:

Package               Current  Wanted  Latest  Location
browser-sync           2.12.8  2.13.0  2.13.0  gulp-book
gulp            4.0.0-alpha.2     git     git  gulp-book
gulp-clean-css          2.0.7  2.0.10  2.0.10  gulp-book
run-sequence            1.1.5   1.2.1   1.2.1  gulp-book
serve-static           1.10.2  1.11.1  1.11.1  gulp-book

Wie schon erwähnt wurde, können outdated Module mit npm update aktualisiert werden. Wenn alles getestet ist und gut läuft, können Sie entscheiden, ob die Versionen in der package.json aktualisiert werden müssen. Nachdem die Versionen aktualisiert wurden, kann eine neue npm-shrinkwrap.json mit Hilfe von shrinkwrap (wie bereits gezeigt) erstellt und unter Versionskontrolle gestellt werden.

An dieser Stelle ist noch das Package Linking mittels npm link zu erwähnen. Damit lassen sich Module verlinken. Angenommen, die Projektstruktur sieht folgendermaßen aus:

demo-project
    module-1
    module-2

Nun führen wir folgendes aus:

cd demo-project/module-2
npm link ../module-1

Wenn man jetzt Änderungen im module-1 macht, werden sie automatisch in module-2/node-modules/module-1 reflektiert. Aus dem Package module-2 heraus lassen sich JavaScript Dateien im module-1 ganz einfach importieren (hier als CommonJS Modul mit require):

var mod1 = require("module-1");
mod1.doSomething();

Wie sucht der require()-Aufruf unter Node.js aber genau nach einer passenden Quelldatei? Angenommen, das Modul heißt module-1. Zuerst wird im Verzeichnis node_modules nach dem Verzeichis module-1 gesucht. Sollte require() kein Verzeichnis node_modules finden, wird es sich, ausgehend vom aktuellen Standort, ein Verzeichnis höher bewegen und dort erneut nach einem Verzeichnis namens node_modules suchen. Ist es vorhanden, sucht require() dort in der gleichen Form nach dem angefragten Modul node_modules. Dieses Verfahren wendet require() bis zum Wurzelverzeichnis des aktuellen Dateisystems an. Wird require() auch dort nicht fündig, wirft sie einen entsprechenden Fehler. Ansonsten, wenn das Verzeichnis module-1 gefunden wird, wertet require() das main Attribut in der Datei package.json aus. Angenommen das main Attribut sieht wie folgt aus:

...
  "main": "lib/module-1",
...

Die Angaben in diesem Attribut definieren einen relativen Pfad zum Basispfad des Modules. Der require()-Aufruf wandelt damit require("module-1") in require('module-1/lib/module-1') um. Wäre diese Angabe in der Datei package.json nicht vorhanden, dann würde require("module-1") noch die Variante require("module-1/index") probieren, so dass die Datei node_modules/module-1/index.js geladen wird, sofern sie existiert. Scheitert auch das, gibt der require()-Aufruf einen Fehler zurück.

NPM Scripts als Build-Tools

Viele Aufgaben von Build-Tools wie Grunt oder Gulp können mit npm scripts erledigt werden. Es handelt sich um einen Abschnitt namens scripts in der package.json Datei. Alle Tools, die in diesem Abschnitt benutzt werden, müssen mit --save-dev installiert werden. Ein Beispiel:

npm install --save-dev node-sass

node-sass kompiliert SASS Dateien und erzeugt daraus CSS Dateien. Nun schreiben wir in der package.json:

"scripts": {
  "scss": "node-sass --output-style compressed -o dist/css src/scss"
}

und führen den folgenden Befehle in der Console aus:

npm run scss

Ergebnis: die SASS Dateien aus dem Verzeichnis src/scss werden nach CSS kompiliert und im dist/css abgelegt. Wie man sieht, hat jeder Befehl im scripts einen Namen, eine Anweisung und kann mit

npm run <befehlsname>

ausgeführt werden. Wozu ist es gut? Damit wird eine Abstraktionsebene eingeführt, weil die Tools nicht direkt, sondern über die scripts Befehle angesprochen werden. Die Komplexität der Tools wird versteckt. Viele Tools verstehen auch die Datei-"globs", wie z.B. *.js, *.min.css oder assets/*/*. Ein Beispiel für Linting könnte so aussehen:

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

Benutzung in der Console:

npm run lint

Die folgende Syntax kann bei den scripts Befehlen unabhängig vom Betriebssystem verwendet werden:

  • && für die sequenzielle Ausführung von Tasks (Verkettung von Tasks)
  • & für die parallele (gleichzeitige) Ausführung von Tasks
  • < um die Inhalte (stdin) einer Datei in einen Befehl einzufügen
  • > um die Ausgabe (stdout) eines Befehls weiterzuleiten und ihn in eine Datei einzufügen
  • | um die Ausgabe (stdout) eines Befehls weiterzuleiten und ihn an einen anderen Befehl zu senden

Weitere Möglichkeiten sind die pre- und post-hooks. Hat z.B. ein scripts Befehl den Namen build, wird der Befehl mit dem Namen prebuild davor und der Befehl mit dem Namen postbuild danach automatisch ausgeführt. Ein Beispiel soll das veranschaulichen:

"devDependencies": {
  "jshint": "latest",
  "stylus": "latest",
  "browserify": "latest"
},
"scripts": {
  "clean": "rimraf dist",
  "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 clean && npm run build:css && npm run build:js",
  "prebuild:js": "npm run lint"
}

Im Beispiel führt npm run build die Befehle clean, build:css und build:js aus. Der Befehl build:js wird aber nicht ausgeführt, bevor der lint Befehl abgearbeitet ist.

Für die Überwachung von geänderten Dateien kann watch benutzt werden. Die folgende Konfiguration dient der Überwachung des ganzen Projektverzeichnisses. Ändert sich eine Datei, werden die HTML-, CSS- und JS-Assets neu gebildet. Man führt einfach npm run build:watch aus und fangt an zu entwickeln.

"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' .",
}

Die parallele Ausführung von Tasks / Prozessen in Form vom cmd1 & cmd2 & cmd3 hat mehrere Nachteile. Für diese Aufgabe kann am besten Parallelshell benutzt werden. Die Nachteile von cmd1 & cmd2 & cmd3 werden auf der Homepage von Parallelshell aufgezeigt.

"devDependencies": {
  ...
  "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 parallel aus. Wenn man z.B. CSS verändert, wird nur CSS neu kompiliert. Wenn man JavaScript verändert, wird nur JavaScript neu kompiliert und so weiter.

Es ist auch denkbar, einen Grunt oder Gulp basierten Build mit scripts zu triggern.

"scripts": {
  "build:prod": "gulp build --prod",
  "build:dev": "gulp build"
}

Alle verfügbaren npm scripts in einem Projekt lassen sich mit dem Befehl npm run auflisten.