Node.js als Helfer für Entwickler – Theorie und Praxis

Datum: 14.04.2020

gridscale node.js
In unserem Tutorial Node.js für Anfänger haben wir euch gezeigt was Node.js ist, wie ihr es installiert und verwendet. In diesem Artikel zeigen wir euch nun wie man Node.js in der Praxis verwenden kann – und zwar konkret als Helferlein in der (Web-)Entwicklung.

Wozu benötige ich Node.js?

Der Alltag eines Webentwicklers hat sich in den letzten Jahren gewandelt: Entwickelte man vor ein paar Jahren noch ein paar lose zusammenhängende – bestenfalls dynamische – Webseiten, welche man entweder “live” auf dem Server modifizierte oder nach kurzem lokalen Test per FTP auf den Produktionsserver übertrug, sind die Anforderungen mittlerweile gewachsen.

Eine Webseite ist heute oft Teil einer ganzen Applikation an der mehrere Entwickler arbeiten. Die Applikation soll versioniert sein, Bugfixes und neue Features sollen möglichst schnell in die laufende Anwendung einfließen und zwar ohne diese zu zerstören oder gar für eine längere Downtime zu sorgen. “Continuous Integration” (CI) und “Continuous Delivery” (CD) sind hier die Schlagwörter.

So sehr diese Praxis die Qualität des Produktes erhöht, so sehr gängelt sie auch die Entwickler mit zusätzlichen Prozessen. Die Gefahr, dass die Entwickler aus Zeitmangel oder Bequemlichkeit den ein- oder anderen Prozess umgehen ist hoch. Abhilfe schaffen hier Tools, die das Entwicklerleben wieder lebenswert machen.

Und warum Node.js?

Der Vorteil an Node.js ist, dass die meisten Entwickler – nicht nur im Webbereich – schon einmal etwas mit Javascript geschrieben haben. Die Grundlagen der Sprache sind also bekannt, zudem ist die Lernkurve bei Javascript nicht sonderlich hoch, und Nachschlagewerke im Internet gibt es wie Sand am Meer.

Node.js kombiniert diese Einfachheit nun mit einer systemnahen Macht. Plötzlich kann ich mit Javascript nicht nur Popups und Animationen für den Browser bauen, sondern kann in das Dateisystem eingreifen, Systemeigenschaften auslesen, Applikationen starten und Shell Befehle ausführen!

Node.js ermächtigt also jeden Programmierer ohne viel Anlauf für sich oder sein Team beliebige Tools zu bauen. Diese Tools können dann alle Kollegen ohne Aufwand auch auf ihren Rechnern ausführen, fast egal welches Betriebssystem sie dabei einsetzen. Auch auf Servern oder CI Runnern z.B. bei Github oder Gitlab können diese Scripts ausgeführt werden. In Kombination mit Docker Containern werden die Tools zu komfortabel verschiffbaren Applikationen – aber dies führt an dieser Stelle zu weit.

Was kann Node.js?

Mit Node.js geschriebene Scripte können zunächst auf alle im aktuellen JavaScript-Standard (ECMAScript) bekannten Funktionen zugreifen. Somit stehen bereits umfangreiche Funktionen, z.B. zur Anzeige und Manipulation von Datum und Zeit, für mathematische Aufgaben oder zur String Manipulation zur Verfügung.

Node.js erweitert diese Möglichkeiten durch jede Menge Zusatzfunktionen, welche in verschiedene Pakete unterteilt sind. So gibt es z.B. das Paket “fs” zur Manipulation des Dateisystems, oder das Paket “child_process” zum Verwalten von Kindprozessen. Diese Module sind also die Brücke zwischen JavaScript und den unteren Systemebenen.

Um eine Funktionalität aus einem der Pakete zu benutzen, muss man das Paket zunächst importieren (require() Funktion). Welche Pakete zur Verfügung stehen, findet ihr in der Dokumentation zu eurer Node.js Version, z.B. hier: https://nodejs.org/dist/latest-v12.x/docs/api/

Drittanbieter Pakete

Neben den in Node.js ohnehin verfügbaren Paketen gibt es noch jede Menge weiterer Pakete anderer Entwickler. Node.js hat mit NPM (=Node Package Manager) einen eigenen Paketmanager inkl. öffentlicher Paket-Datenbank auf https://npmjs.com. Das Angebot freier und kostenpflichtiger Pakete ist riesig. Pakete lassen sich über die Suche auf npmjs.com finden.

Installiert werden diese dann mit dem npm Kommandozeilen-Tool, welches bei der Installation von Node.js direkt mitinstalliert wird. Dabei gibt es zum Einen die Möglichkeit Pakete global zu installieren. Dies bietet sich für Pakete an, welche projektübergreifend direkt auf der Kommandozeile ausgeführt werden sollen.

Zum Anderen kann man die Pakete projektbezogen installieren und das Paket mit der gewünschten Version als “requirement” in einer Datei namens package.json eintragen lassen. Diese Datei wird dann mit dem Projekt in die Versionsverwaltung eingecheckt und ermöglicht anderen Entwicklern somit per simplen Aufruf von npm i alle benötigten Abhängigkeiten für eure Anwendung zu installieren.

Paket global installieren (-g Flag):
npm i -g sass

Paket projektbezogen installieren und in package.json speichern (–save Flag)
npm i –save @gridscale/api

Einige Pakete werden nur zum entwickeln, z.B. für zum Testen eurer Applikation benötigt. Diese könnt ihr anstelle des –save Flags noch mit dem –save-dev Flag installieren. Diese Pakete werden bei einem “npm i –prod” nicht mitinstalliert.

Beispiele aus der Praxis

Beispiel 1: Deployment

Nehmen wir an, ihr habt eine Applikation, welche für den Produktionsbetrieb einige Build-Prozesse durchlaufen muss.

Beginnt euer Script, indem ihr als Erstes ein paar Konfigurationen festlegt und alle benötigten Pakete einbindet:

 
// global settings
const DIR_DIST = __dirname + '/dist/';
const DIR_JS = __dirname + '/js/';
const FILE_MAIN_SCSS = __dirname + '/css/main.scss';
 
// import 1st party requirements (are included in Node.js)
const childProcess = require('child_process'); // required to spawn additonal processes / execute shell commands
const events = require('events'); // required for event handling
const fs = require('fs'); // required for file system operations
const os = require('os'); // required to gather information from operating system (we use it to get the username)
const readline = require("readline").createInterface({
   input: process.stdin,
   output: process.stdout
}); // required (and set up) to gather some input from the user
 
// import 3rd party requirements (have to be installed via `npm i --save-dev browserify sass`)
const browserify = require('browserify'); // this compiles our javascript files, including all dependencies
const sass = require('sass'); // this compiles our CSS files
  



Da es sich für ein solches Script anbietet, wählen wir einen sequentiellen Programmierstil. Das Script wird also von oben nach unten abgearbeitet. Allerdings ist JavaScript eigentlich eine asynchrone Programmiersprache – d.h. Operationen, welche nicht sofort durchgeführt werden können, wie z.B. Dateioperationen blockieren normalerweise nicht die Ausführung des Scripts, sondern rufen nach Fertigstellung Callbacks auf, bzw. arbeiten mit sogenannten Promises. Somit wäre kein sequentieller Ablauf möglich, da das Programm einfach weiterlaufen würde, obwohl das Kompilieren des JavaScript noch im Hintergrund läuft.

Node.js bietet für die eigenen Pakete daher oft eine synchrone Variante der einzelnen Methoden an – es gibt also neben einem “fs.writeFile()”, welches mit Callbacks arbeitet, auch noch ein “fs.writeFileSync()”, welches die Ausführung des Programms so lange anhält bis die Operation fertiggestellt ist und das Ergebnis als Return-Value zurückliefert. Im Fehlerfall wird hier eine Exception geworfen und – wenn diese nicht gesondert behandelt wird – das Programm damit abgebrochen. Diese …Sync() Varianten existieren für die meisten Node.js Methoden.

Für die Methoden, welche diese synchronen Varianten nicht anbieten, machen wir uns die await Syntax zu nutze. Dazu kapseln wir den folgenden Programmcode in eine anonyme, asynchrone Funktion:

 
(async function () {
    // ensure that dist directory exists and is empty
   childProcess.execSync('mkdir -p ' + DIR_DIST); // we use shell command here, because this silently quits if directory exists
   childProcess.execSync('rm -rf ' + DIR_DIST + '*'); // we use shell command here, because recursive removal is experimental in `fs`
 
   // - the following code is inserted here -
})(); 


Jetzt aber zurück zu unserem Script. Die Applikation benötigt einige Drittanbieter Bibliotheken, deren Versionen genau in der package.json Datei, welche ihr in eurem Repository mitliefert, festgelegt sind. Ihr müsst sicherstellen dass jeder Entwickler die Applikation mit den korrekten Bibliotheken in den richtigen Versionen baut. Hierzu führen wir einfach den Node.js Paketmanager `npm` aus unserem Script aus. Dieser installiert oder aktualisiert alle benötigten Pakete:

 
// ensure all packages of the main app are up-to-date before building
   childProcess.execSync('npm i --prod', { cwd: __dirname });



Bevor die Applikation gebaut wird, soll der Entwickler noch angeben, ob es sich um ein Patch-, Minor oder Major Release handelt und welche Features oder Bugfixes in diesem Release enthalten sind:

 
   var releaseType;
   var releaseDescription;
   readline.question("What kind of release are you deploying (P)atch, (M)inor, M(a)jor?\n", _releaseType => {
       if (!_releaseType.match(/^[PMA]$/i)) throw new Error('Invalid answer!');
       releaseType = _releaseType;
 
       readline.question("Short description of whats included in the release?\n", _releaseDescription => {
           if (!_releaseDescription.match(/^(\w+\s?){10,}/)) throw new Error('Please enter a short description, minimum 10 characters!');
           releaseDescription = _releaseDescription;
 
           readline.close(); // resolves the promise we are waiting for further execution
       });
   });
   await events.once(readline, 'close');
 
switch (releaseType.toLowerCase()) {
       case 'p':
           childProcess.execSync('npm version patch', { cwd: __dirname });
           break;
       case 'm':
           childProcess.execSync('npm version minor', { cwd: __dirname });
           break;
       case 'a':
           childProcess.execSync('npm version major', { cwd: __dirname });
           break;
   }
 


Anschließend wird die Applikation gebaut. Hier bauen wir eine fiktive Applikation indem wir einfach mit Javascript Browserify (Ein Tipp übrigens, wenn man NPM Pakete auch im Javascript der Web-Applikation nutzen möchte), und SCSS mit Sass kompilieren:

 

   // collect all the javascript files we have and add to browserify
   var b = browserify();
   fs.readdirSync(DIR_JS)
       .forEach(file => b.add(DIR_JS + file));
   // compile javascript!
   var distJS = fs.createWriteStream(DIR_DIST + 'main.js');
   b.bundle().pipe(distJS);
   await events.once(distJS, 'close'); // wait with further execution until stream is closed
 
 
   // compile sass to CSS, using the `sass` package
   var sassResult = sass.renderSync({file: FILE_MAIN_SCSS});
   fs.writeFileSync(DIR_DIST + 'style.css', sassResult.css);
 
   // create a ZIP file from our dist dir
   childProcess.execSync('zip ' + __dirname + '/dist.zip ' + DIR_DIST + '*');
 


Nachdem die Applikation gebaut ist, sollen einige grundsätzliche Integrationstests durchgeführt werden und nur bei Erfolg ein Deployment auf den Produktionsserver initiiert werden. Das Deployment funktioniert in unserer Beispiel Applikation indem wir einen POST HTTP Request an einen fiktiven Deploy-Service senden.

Diesem übermitteln wir nicht nur die Applikation als ZIP Datei, sondern auch noch das Ergebnis des Integrationstests, und den Namen des Entwicklers der den Deploy angestoßen hat sowie die vom Entwickler eingegebene Beschreibung des Release:

 
// run integration tests
   childProcess.execSync('myTestSuite --resultfile=' + __dirname + '/tmp/integrationtest.json'); // "myTestSuite" is a placeholder here - testing is out of scope for this article
   var testResults = JSON.parse(fs.readFileSync(__dirname + '/tmp/integrationtest.json', 'utf8'));
 
   if (testResults.tests === testResults.passed) {
       // all tests passed, trigger the deployment
 
       // initialize POST request to our deploy service
       const req = https.request('https://myUrl.com/myDeployService', {
           method: 'POST',
           headers: {
               'Content-Type': 'application/json'
           }
       }, (result) => {
           console.log('Deploy Service result: ' + result.statusCode);
       });
 
       req.on('error', (e) => {
           console.error(`problem with deploy request: ${e.message}`);
       });
 
 
       // set request body
       req.write(JSON.stringify({
           deployer: os.userInfo().username,
           testResults: testResults,
           releaseDescription: releaseDescription,
           applicationZip: fs.readFileSync(__dirname + '/dist.zip')
       }));
      
       // executes the request
       req.end();
   }


Damit ist unser Deploy-Script fertig. Ohne Script wäre das Deployment unser Applikation für jeden Entwickler ein aufwändiger und fehleranfälliger Prozess. Das vollständige Beispiel findest du hier: https://github.com/gridscale/tutorials/tree/master/nodejs-as-developer-tool/Sample1-Deploy

Hinweis: Unser Script benutzt einige Shell Befehle welche auf Windows Systemen evtl. anders heißen, z.B. `mkdir`. Für Windows Systeme müsst ihr das Script daher ggf. Anpassen. Das Script verwendet zudem einige Platzhalter Kommandos und URLs, und ist somit nicht sofort lauffähig.

Beispiel 2: Kundenumgebung umschalten

Ein weiteres schnelles Script dient nur dazu, während der Entwicklung schnell mal die Applikation für verschiedene Kunden zu testen. Stellt euch vor, ihr stellt allen Kunden zwar die Selbe Applikation bereit, allerdings mit angepasstem Layout in Firmenfarben, sowie einigen speziellen Einstellungen.

Dieses Script kommt sogar komplett ohne 3rd party Pakete aus:

 
const DIR_APP = __dirname + '/app/';
const DIR_CUSTOMER = __dirname + '/customers/';
const DIR_DIST = __dirname + '/dist/';
 
const childProcess = require('child_process');
const fs = require('fs');
const http = require('http');
const sass = require('sass');
 
 
// gather customer key from argument
var customer = process.argv[2] || undefined; // `process` is a global available variable
 
if (customer !== undefined && !fs.existsSync(DIR_CUSTOMER + '/' + customer)) {
   throw new Error('Customer ' + customer + ' does not exist');
}
 
// create temp dir and ensure it's empty
childProcess.execSync('mkdir -p ' + DIR_DIST);
childProcess.execSync('rm -rf ' + DIR_DIST + '*');
 
// copy app files to temp dir
childProcess.execSync('cp ' + DIR_APP + 'index.html ' + DIR_DIST);
childProcess.execSync('cp -r ' + DIR_APP + '*.scss ' + DIR_DIST);
childProcess.execSync('cp -r ' + DIR_APP + '*.js ' + DIR_DIST);
 
if (customer !== undefined) {
   childProcess.execSync('cp ' + DIR_CUSTOMER + '/' + customer + '/_theme.scss ' + DIR_DIST);
}
 
// compile sass
var sassResult = sass.renderSync({ file: DIR_DIST + 'style.scss' });
fs.writeFileSync(DIR_DIST + 'style.css', sassResult.css);
 
// merge settings json
var baseJSON = JSON.parse(fs.readFileSync(DIR_APP + 'settings.json', 'utf8'));
var customerJSON = {};
if (customer !== undefined) {
   customerJSON = JSON.parse(fs.readFileSync(DIR_CUSTOMER + '/' + customer + '/settings.json', 'utf8'));
}
 
var mergedJSON = Object.assign(baseJSON, customerJSON);
fs.writeFileSync(DIR_DIST + 'settings.json', JSON.stringify(mergedJSON));
 
// start simple webserver
http.createServer(function (req, res) {
   var file = DIR_DIST.replace(/\/$/, '') + req.url || '/index.html';
   fs.readFile(file, function (err, data) {
       if (err) {
           console.error(file, ' not found');
           res.writeHead(404);
           res.end(JSON.stringify(err));
           return;
       }
       console.info('Serving ', file)
       res.writeHead(200);
       res.end(data);
   });
}).listen(8080);
 
console.info('Open http://localhost:8080 to see the result');



Das vollständige, lauffähige Beispiel findest du hier: https://github.com/gridscale/tutorials/tree/master/nodejs-as-developer-tool/Sample2-CustomerEnvironment

Automatisierung von Aufgaben – einfach und portabel

Wer mit JavaScript grundsätzlich umgehen kann und eine einfache und portable Möglichkeit sucht um bestimmte wiederkehrende Aufgaben zu automatisieren oder zumindest zu vereinfachen, sollte sich überlegen ob Node.js eine Lösung sein kann.

Wer nicht gerade alles selbst scripten möchte, für den gibt verschiedene Tools, so genannte “Task runner” welche auf Node.js basieren. Beispiele sind hier grunt oder gulp. Hier gibt es schon eine gewisse vorgegebene Script-Struktur und viele gebräuchliche Aufgaben wurden bereits in Addons für diese Tools gegossen.

Fröhliches scripten!


Jan Stuhlmann

Jan Stuhlmann | Frontend, UX & GUI
Jan erstellte bereits während der Schulzeit nebenbei Webseiten- und Applikationen. Im Jahr 2007 machte er schließlich sein Hobby zum Beruf. Bei gridscale kümmert er sich hauptsächlich um das Frontend der verschiedenen Panels, hilft aber auch gerne mal im Backend und im Bereich der Webseite aus.

Zurück zur Übersicht