8. Die Welt des Entwicklers

Moderne Softwareentwicklung ist heute oft damit verbunden, den fachlichen Anspruch durch geschicktes Einsetzen von vorhandenen Bausteinen zu erreichen. Das Mantra dazu lautet:

DRY

Und das steht für “Don’t Repeat Yourself”. Du sollst dich also nicht selbst wiederholen. Was es gibt, wird benutzt. npm als wichtigstes Repository für Biblitoheken bietet alleine über 300.000 Code-Blöcke. Vieles davbon ist nicht oder nur selten sinnvoll, aber es bleiben Hunderte wichtige Bausteine.

8.1 Einsatz

Der Einsatz erfolgt auf drei Ebenen:

  1. Zum Entwicklungszeitpunkt zur Automation der Entwicklungsaufgaben
  2. Auf dem Server zum Ablaufen lassen der Anwendung
  3. Im Client, wenn deine App beim Benutzer ausgeführt wird

Diese Einsatzfälle werden in diesem Kapitel betrachtet. Viele Bibliotheken sind sehr umfangreich und nicht alles wird benötigt. Hier geht es vor allem darum, eine Übersicht zu bekommen und Zugang zu den Dokumentationen zu erhalten. Eine vollständige Darstellung ist Online und in weiterer Literatur zu finden.

Die Auswahl ist auch weitgehend davon geprägt, was aktuell am Markt gut nachgefragt wird, was sich zügig entwickelt und bereits breit eingesetzt wird. Außerdem sind alle Werkzeuge und Bibliotheken freie Software (open source) und damit ohne weitere Kosten jederzeit verfügbar. Aufgrund dieser Rahmenbedingungen erhebt die Darstellung keinen Anspruch auf Vollständigkeit.

8.2 Die Entwicklungsumgebung

Paketverwaltung mit npm

Build-Automation mit gulp

Ein wichtiger Bestandteil der Entwicklungsumgebung ist die Automation beim Erstellen der auslieferfähigen Form. Dabei geht es um immer wieder erforderliche Vorgänge, die möglichst schnell und einfach ablaufen sollen. Konkrete Aufgaben sind:

Die Liste lässt sich sicher weiter erweitern und ist in jedem Projekt etwas anders oder hat andere Schwerpunkte. Da in diesem Buch der Schwerpunkt auf dem JavaScript-Ökosystem liegt, ist es naheliegend, dass auch für den Automationsvorgang JavaScript oder TypeScript benutzt wird. Beispielhaft soll hier gulp vorgestellt werden, zu finden auf http://gulpjs.com/.

Installation

Die Installation erfolgt über npm. Ein explizites Herunterladen gibt es für gulp nicht. Neben der reinen Ausführumgebung liefert gulp eine eigene, NodeJS-basierte, Kommandozeile, gulp-cli. Um gulp zu nutzen, sind zwei Eingaben erforderlich:

1 $ npm install gulp-cli -g
2 $ npm install gulp -D

Dann ist noch eine Steuerdatei erforderlich, die immer gulpfile.js heißt. Diese kann, so wie bei jeder anderen Node-Anwendung, weitere Module aufrufen, sodass sich eine Build-Umgebung gut modularisieren lässt.

Erste Schritte

Eine typische erste Steuerdatei könnte folgendermaßen aussehen:

 1 var gulp = require('gulp');
 2 var pug = require('gulp-pug');
 3 var less = require('gulp-less');
 4 var minifyCSS = require('gulp-csso');
 5 
 6 gulp.task('html', function(){
 7   return gulp.src('client/templates/*.pug')
 8     .pipe(pug())
 9     .pipe(gulp.dest('build/html'))
10 });
11 
12 gulp.task('css', function(){
13   return gulp.src('client/templates/*.less')
14     .pipe(less())
15     .pipe(minifyCSS())
16     .pipe(gulp.dest('build/css'))
17 });
18 
19 gulp.task('default', [ 'html', 'css' ]);

Eine gulp-Datei besteht primär aus Tasks (Aufgaben). Diese können Aktionen ausführen oder andere Tasks aurufen. Im Beispiel gibt es eine Standardaufgabe ‘default’. Diese wird ausgeführt, wenn keine bestimmte Aufgabe aufgerufen wird. Im Beispiel werden damit zwei andere Aufgabe ausgelöst: ‘html’ und ‘css’. Die Namen sind frei wählbare Zeichenketten.

gulp nutzt Streams in Node zum Ausführen von Dateizugriffen. Die wichtigsten Funktionen sind damit:

Ein weiterer, spezieller Befehl ist watch, mit dem sich Dateien dauerhaft überwachen lassen und bei Änderungen wird die betreffende Aufgabe sofort ausgelöst.

Hier wird schon deutlich, worauf es ankommt. Zum einen werden sogenannte Plug-Ins benötigt, die konkrete Aufgabe ausführen. Diese sollten Streams lesen und wieder zurückgeben. Zum anderen basiert das Einlesen auf einem Dateimuster, sodass möglichst viele Dateien in einem Schritte behandelt werden können.

Dateien lesen

Das Lesen nutzt das Modul node-glob. Glob nutzt eine allgemeine Mustererkennungstechnik. Kurz zusammengefasst sind dies folgende Zeichen:

Dateien oder Verzeichnisse, die mit einem Punkt beginnen werden, von den globalen Muster nicht erfasst. Das ist der Grund dafür, warum einige Steuerdateien, wie .gitignore der .bowerrc mit einem Punkt beginnen. Damit wird das Verhalten von Werkzeugen kontrolliert und dies hat keine Bedeutung im Erstellungsprozess.

src() kann neben der direkten Angabe auch ein Array mit mehreren Mustern annehmen, die nacheinander ausgeführt werden. Beginnt ein Muster !, wird der gesamte Ausdruck aus Ausschlusskriterium benutzt. Typisch ist folgende Vorgehensweise:

1 gulp.src(['/src/\*.js', '!/src/\*.min.*,js'])

Hier werden pauschal alle JavaScript-Dateien eingelesen, aber nicht solche, die die Zeichenfolge .min. enthalten.

Wenn das Muster nichts ergibt, wird ein leeres Array zurückgegeben. Das ist insofern kritisch, als das ein Fehler im Muster nicht zu einer Fehlermeldung führt, denn das leere Array ist ein gültiges Objekt für den pipe-Befehl. Er tut dann nur nichts.

Nocheinmal zurück zum Beispiel am Anfang:

1 gulp.task('html', function(){
2   return gulp.src('client/templates/*.pug')
3     .pipe(pug())
4     .pipe(gulp.dest('build/html'))
5 });

Die Aufgabe ‘html’ ließt alle Dateien mit der Dateiendung .pug aus dem Ordner client/templates – dieser liegt relativ zum gulpfile.js. Alle Dateien werden einzeln und mit dem vollständigen Pfad an das Plugin pug() übergeben. Dessen Aufgabe ist es hier, die in Pug erstellten Vorlagen in HTML zu konvertieren. Wenn Sie einen Server mit NodeJs betreiben, erfolgt dies dynamisch im Rahmen der Serversoftware. Ist ein solcher Server nicht vorhanden, nicht geplant oder unpassend, musst du aber nicht auf Pug verzichten. Freilich geht die Dynamik teilweise verloren. Die fertigen HTML-Dateien werden in den Pfad builg/html kopiert. Der Befehl dest bekommt als Parameter immer einen Pfad, nie einen bestimmten Dateinamen. Wird als Ausgabe eine einzelne Datei beötigt, muss ein weiteres Plugin benutzt werden, dass die Zusammenfassung der Eingaben nach dem gewünschten Verfahren kennt.

Die zweite Aufgabe ist noch umfangreicher:

1 gulp.task('css', function(){
2   return gulp.src('client/templates/*.less')
3     .pipe(less())
4     .pipe(minifyCSS())
5     .pipe(gulp.dest('build/css'))
6 });

Hier werden gleich zwei Aufgaben ausgeführt. less() übersetzt in LESS geschriebene Stile in CSS. Die CSS-Dateien werden als Stream – also im Speicher – an minifyCSS() weitergeleitet, wo sie verdichtet werden. Bei CSS umfasst dies das Entfernen von Kommentaren und Leerzeichen sowie Zeilenumbrüchen. Dies spart später Bandbreite und kommt der Performance zugute.

Die Plugins

Das Werkzeug gulp lebt von einer starken Plugin-Welt. Erreichbar sind alle Plugins über npm. Damit das vorherige Beispiel funktioniert, musst du also folgendes auf der Kommandozeile eingeben:

1 $ npm i gulp-pug --save-dev
2 $ npm i gulp-less --save-dev
3 $ npm i gulp-csso --save-dev

Die Ablage mit –save-dev zeigt an, welche Werkzeuge benutzt werden und damit kann der Entwicklerstack zwischen mehreren Entwicklern über die package.json synchronisiert werden. Das es sich lediglich um Entwicklungswerkzeuge handelt und diese keinen Eingang in die später bereitgestellte Anwendungsumgebung finden, werden sie in einem speziellen Entwicklerzeig gehalten (statt mit –save allgemein).

Gulp und TypeScript

Gulp mit JavaScript reicht oft aus. Aber wenn schon mit TypeScript entwickelt wird, wird der eine oder andere doch gerne auch die Automation damit erstellen. Zum Glück ist dies sehr einfach.

An dem gulpfile.js führt dabei erstmal kein Weg vorbei. Aber es kann auch folgendermaßen aussehen:

1 'use strict';
2 
3 const path = require('path');
4 
5 require('ts-node').register({
6   project: path.join(__dirname, 'tools/gulp')
7 });
8 
9 require('./tools/gulp/gulpfile');

Das entscheidende Plugin ist hier ts-node. Dieses Plugin liest alle Dateien aus dem Pfad ein, der als Parameter project übergeben wurde. __dirname ist der Stammpfad einer Node-Anwendung, dies ist eine globale Konstante. Mehr dazu findest du im Kapitel zu Node. tools/gulp ist hier ein willkürlich gewählter Pfad zu Modulen, die jeweils einzelne Aufgaben übernehmen. Jedes Datei ist dort eine .ts-Datei. Der Einstiegspunkt wird dann als Modul geladen. gulpfile lädt eine Datei mit dem Namen gulpfile.ts. Die Dateierweiterung .ts darf nicht angegeben werden, sondern wird implizit benutzt. Die Datei sieht nun folgendermaßen aus:

Listing: gulpfile.ts
1 import './tasks/clean';
2 import './tasks/build';

Es sind also nur zwei weitere Module, die geladen werden. Die Aufgabe zum Aufräumen könntest du so schreiben:

Listing: tools/gulp/clean.ts
1 import { task } from 'gulp';
2 const gulpClean = require('gulp-clean');
3 
4 function cleanTask(glob: string) {
5   return () => gulp.src(glob, { read: false }).pipe(gulpClean(null));
6 }
7 
8 task('clean', cleanTask('dist'));

Das sieht nur wenig anders aus als bei JavaScript. Der Vorteil ist die Typsicherheit bei Parametern. Statt gulp.task() wird der Typ task importiert und dann direkt benutzt (Zeile 8). Die Aufräumaufgabe nutzt dieselben Techniken wie bisher, src, pipe und ein weiteres Plugin, hier gulp-clean. Auch dieses muss wie bisher via npm beschafft werden. Als weitere Module brauchst du also folgendes:

1 $ npm i ts-node --save-dev
2 $ npm i gulp-clean --save-dev

Es bietet sich an, wiederkehrende Aufgaben gleich zu modularisieren. Dies umfasst:

Konstanten definieren

Folgende Datei zeigt, wie eine solche Definition aussehen könnte:

Listing: tools/gulp/constants.ts
 1 import {join} from 'path';
 2 
 3 export const MY_VERSION = require('../../package.json').version;
 4 
 5 export const PROJECT_ROOT = join(__dirname, '../../');
 6 export const SOURCE\_ROOT = join(PROJECT_ROOT, 'src/');
 7 
 8 export const SASS\_AUTOPREFIXER_OPTIONS = {
 9   browsers: [
10     'last 2 versions',
11     'not ie <= 10',
12     'not ie_mob <= 10',
13   ],
14   cascade: false,
15 };
16 
17 export const HTML\_MINIFIER_OPTIONS = {
18   collapseWhitespace: true,
19   removeComments: true,
20   caseSensitive: true,
21   removeAttributeQuotes: false
22 };
23 
24 export const LICENSE_BANNER = `/**
25   * @license MyFancyProject v${MY_VERSION}
26   * Copyright (c) 2011-2017 My Company Name, www.mywebsite.com
27   * License: ICS 
28   */`;
29 
30 export const NPM\_VENDOR\_FILES = [
31   '@angular', 'core-js/client', 'rxjs', 'systemjs/dist', 'zone.js/di\
32 st'
33 ];

In der Aufgabendatei können diese Konstanten dann benutzt werden.

Testen mit Jasmine und Karma

Unit Tests gehören zu jeder Applikation. Da die Applikationslogik in JavaScript bzw. TypeScript vorliegt, sollte dieser Teil unbedingt getestet werden. Vor allem bei Projekten, die einen größeren Umfang mit hunderten Funktionen haben, ist es praktisch unmöglich, nach jeder Änderung alle Funktionen manuell zu prüfen.

Es gibt eine ganze Reihe von Testwerkzeugen, von denen hier einige beispielhaft vorgestellt werden. Andere funktionieren aber meist ähnlich, sodass sich dieser Text gut übertragen lässt.

Die Motivation für die Auswahl hier ist der Verbreitung geschuldet. Je mehr Anwender es für eine Umgebung gibt, desto mehr Informationen sind verfügbar. Damit ist auch der Einstieg einfacher – was wieder zu mehr Anwendern führt. Ein Kreislauf, der oft über Erfolg oder Misserfolg eines Open Source Projekts entscheidet.

Abhängigkeiten

Wie immer sind am Anfang einige Bibliotheken zu beschaffen. NodeJS, npm und damit die passende Projektumgebung wird hier vorausgesetzt. Die vorhergehenden Kapitel haben dazu reichlich Informationen enthalten.

Folgende Bausteine sind sinnvoll:

Dieser Abschnitt geht davon aus, dass TypeScript benutzt wird. Die Installation der Bibliotheken geht folgendermaßen:

1 $ npm install http-server jasmine-core karma karma-chrome-launcher k\
2 arma-coverage karma-jasmine remap-istanbul --save-dev
Einrichtung der Skripte

In der Entwicklungsumgebung nutzt du Skripte zum Ausführen diverser Aufgaben. Meist betraf dies Gulp. Nun wird npm direkt eingesetzt, um Skripte auszuführen. Dazu wird die Datei package.json so angepasst, dass der Abschnitt ‘scripts’ folgendermaßen aussieht:

Listing: package.json für das Testen
1 "scripts": {
2   "build": "rm -rf dist && tsc -p src",
3   "test": "karma start karma.conf.js",
4   "pretest": "npm run build && npm run test",
5   "posttest": "node_modules/.bin/remap-istanbul -i coverage/coverage\
6 -final.json -o coverage -t html",
7   "testsuite": "http-server -c-1 -o -p 8875 .",
8   "coverage": "http-server -c-1 -o -p 9875 ./coverage"
9 },

Diese sechs Befehle haben folgende Bedeutung:

TypeScript einrichten

TypeScript muss in einer bestimmten Form konfiguriert werden, damit es mit den Tests funktioniert. Dies ist normalerweise der Fall, aber du solltest dich für diese Anleitung nochmal damit auseinandersetzen. Die Datei tsconfig.json enthält mindestens folgende Optionen:

tsconfig.json
 1 {
 2   "compilerOptions": {
 3     "target": "ES5",
 4     "module": "commonjs",
 5     "moduleResolution": "node",
 6     "emitDecoratorMetadata": true,
 7     "experimentalDecorators": true,
 8     "sourceMap": true,
 9     "removeComments": true,
10     "declaration": true,
11     "outDir": "../dist"
12   },
13   "exclude": [
14       "node_modules"
15   ]
16 }

Wichtig ist vor allem das Ausgabeverzeichnis ‘outdir’. Dies ist der Ordner, der mit jedem Erstellen geleert wird und wo die erstellten JavaScript-Dateien zu finden sind.

Karma einrichten

Der Name ist Programm – wenn du als Entwickler Karma benutzt bekommst du ein gutes Karma. Neben Karma wird karma-test-shim benötigt, damit das Laden mit SystemJS gelingt. SystemJS ist ein häufig benutzter Loader, der JavaScript-Dateien dynamisch nachlädt. Er wird standardmäßig von Angular eingesetzt. Es ist sinnvoll, diesen parat zu haben.

Karma hat eine eigene Konfiguration, die Datei karma.conf.js. Ähnlich wie bei Gulp hast du hier keine reine JSON-basierte Konfiguration, sondern eine programmierte Datei.

Listing: karma.conf.js
 1 module.exports = function(config) {
 2     config.set({
 3 
 4         basePath: '.',
 5 
 6         frameworks: ['jasmine'],
 7 
 8         files: [
 9             // Pfade geladen durch Karma
10             {pattern: 'node_modules/angular2/bundles/angular2-polyfi\
11 lls.js', included: true, watched: true},
12             {pattern: 'node_modules/systemjs/dist/system.src.js', in\
13 cluded: true, watched: true},
14             {pattern: 'node_modules/rxjs/bundles/Rx.js', included: t\
15 rue, watched: true},
16             {pattern: 'node_modules/angular2/bundles/angular2.dev.js\
17 ', included: true, watched: true},
18             {pattern: 'node_modules/angular2/bundles/testing.dev.js'\
19 , included: true, watched: true},
20             {pattern: 'karma-test-shim.js', included: true, watched:\
21  true},
22 
23             // Pfade mit Importen
24             {pattern: 'dist/**/*.js', included: false, watched: true\
25 },
26 
27             // Debugger Unterstützung
28             {pattern: 'src/**/*.ts', included: false, watched: false\
29 },
30             {pattern: 'dist/**/*.js.map', included: false, watched: \
31 false}
32         ],
33 
34         // Proxies
35         proxies: {
36             // Komponentenloader für Angular
37             '/src/': '/base/src/'
38         },
39 
40         port: 9876,
41 
42         logLevel: config.LOG_INFO,
43 
44         colors: true,
45 
46         autoWatch: true,
47 
48         browsers: ['Chrome'],
49 
50         // Karma Plug-Ins
51         plugins: [
52             'karma-jasmine',
53             'karma-coverage',
54             'karma-chrome-launcher'
55         ],
56 
57         // Coverage (Testabdeckung)
58         reporters: ['progress', 'dots', 'coverage'],
59 
60         // Quellen zum berechnen der Testabdeckungen, sollte
61         // keine Bibliotheken und keine Testfunktionen enthalten
62         preprocessors: {
63             'dist/**/!(*spec).js': ['coverage']
64         },
65 
66         coverageReporter: {
67             reporters:[
68                 {type: 'json', subdir: '.', file: 'coverage-final.js\
69 on'}
70             ]
71         },
72 
73         singleRun: true
74     })
75 };

Hier gibt es einiges zu beachten:

Alles andere ist verhältnismäßig einfach und verhält sich so, wie es anhand der Namen und Kommentare zu erwarten ist.

Die Kompatiblität zwischen Karma und Angular benötigt eine kleine Hilfsdatei. Dies passiert oft, weil die Entwickler der Werkzeuge sich zwar durchaus für andere interessieren, aber im Eifer der Entwicklung nicht immer alles passend bereitstellen können.

Listing: karma-test-shim.js
 1 // Voller Stack für einfachere Fehlersuche
 2 Error.stackTraceLimit = Infinity;
 3 
 4 jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000;
 5 
 6 // Synchronen Start verhindern,
 7 // später wird `__karma__.start()` benutzt.
 8 __karma__.loaded = function() {};
 9 
10 System.config({
11     packages: {
12         'base/dist': {
13             defaultExtension: false,
14             format: 'cjs',
15             map: Object.keys(window.__karma__.files).filter(onlyAppF\
16 iles).reduce(createPathRecords, {})
17         }
18     }
19 });
20 
21 System.import('angular2/src/platform/browser/browser_adapter')
22     .then(function(browser_adapter) { browser_adapter.BrowserDomAdap\
23 ter.makeCurrent(); })
24     .then(function() { return Promise.all(resolveTestFiles()); })
25     .then(function() { __karma__.start(); }, function(error) { __kar\
26 ma__.error(error.stack || error); });
27 
28 function createPathRecords(pathsMapping, appPath) {
29     // creates local module name mapping to global path with karma's\
30  fingerprint in path, e.g.:
31     // './vg-player/vg-player':
32     // '/base/dist/vg-player/vg-player.js?f4523daf879cfb7310ef624268\
33 2ccf10b2041b3e'
34     var pathParts = appPath.split('/');
35     var moduleName = './' + pathParts.slice(Math.max(pathParts.lengt\
36 h - 2, 1)).join('/');
37     moduleName = moduleName.replace(/\.js$/, '');
38     pathsMapping[moduleName] = appPath + '?' + window.__karma__.file\
39 s[appPath];
40     return pathsMapping;
41 }
42 
43 function onlyAppFiles(filePath) {
44     return /\/base\/dist\/(?!.*\.spec\.js$).*\.js$/.test(filePath);
45 }
46 
47 function onlySpecFiles(path) {
48     return /\.spec\.js$/.test(path);
49 }
50 
51 function resolveTestFiles() {
52     return Object.keys(window.__karma__.files)  // All files served \
53 by Karma.
54         .filter(onlySpecFiles)
55         .map(function(moduleName) {
56             // loads all spec files via their global module names (e\
57 .g.
58             // 'base/dist/vg-player/vg-player.spec')
59             return System.import(moduleName);
60         });
61 }

Hier sind eigentlich nur Anpassungen an die tatsächlich benutzten Pfade notwendig. Ansonsten liegt die Datei im Wurzelverzeichnis der Applikation und wartet darauf, geladen zu werden. Dazu wird freilich eine Applikation benötigt.

Die Applikation

Die Applikation hier ist rein client-orientiert. Die möglichen serverseitigen Dienste spielen erstmal keine Rolle, um es nicht unnötigt kompliziert zu machen.

In Angular gibt es viele Arten von Objekten, aber drei sind besonders darauf ausgerichtet, testbar zu sein: Komponenten (components), Dienste (services) und Filter (pipes).

Zuerst wird ein Dienst erstellt:

Listing: src/services/my-service.tsJavaScript
 1 import {Injectable} from '@angular/core';
 2 
 3 @Injectable()
 4 export class MyService {
 5     animals:Array<string>;
 6 
 7     constructor() {
 8         this.animals = ['golden retriever', 'french bulldog', 'germa\
 9 n shepherd', 'alaskan husky', 'jack russel terrier', 'boxer', 'chow \
10 chow', 'pug', 'akita', 'corgi', 'labrador'];
11     }
12 
13     getDogs(count:number) {
14         var result = [];
15 
16         if (count > this.animals.length) count = this.animals.length;
17 
18         for (var i=0; i<count; i++) {
19             result.push(this.animals[i]);
20         }
21 
22         return result;
23     }
24 }

Um ein Gefühl für Tests zu bekommen, solltest du zuerst die Flusszweige im Code verstehen. Die Methode getDogs enthält eine if-Anweisung. Der Codefluss geht also entweder durch diesen Teil oder eben nicht. Eine hohe Testabdeckung wird erreicht, indem die Tests jeden Zweig erreichen. Hier ist der Zweig an eine Bedingung gebunden, und deren Zustände gilt es im Test zu simulieren.

Hier nun ein Filter als ein weiteres Beispiel:

Listing: src/pipes/my-pipe.ts
 1 import {PipeTransform, Pipe, Injectable} from '@angular/core';
 2 
 3 @Injectable()
 4 @Pipe({name: 'capitalizeWords'})
 5 export class MyPipe implements PipeTransform {
 6     constructor() {
 7     }
 8 
 9     transform(text:string, args:any[]):any {
10         return text.split(' ').map((str) =&gt; {
11             return str.charAt(0).toUpperCase() + str.slice(1);
12         }).join(' ');
13     }
14 }

Hier gibt es nur eine Methode zu testen. Nun folgt eine Komponente, die sowohl den Dienst als das Filter benutzt. Es ist die einzige Komponenten der Applikation.

Listing: src/comps/my-list.tsJavaScript
 1 import {Component, OnInit} from '@angular/core';
 2 import {MyService} from "../services/my-service";
 3 import {MyPipe} from "../pipes/my-pipe";
 4 
 5 @Component({
 6     selector: 'my-list',
 7     bindings: [MyService],
 8     pipes: [MyPipe],
 9     template: `<ul><li *ngFor="let item of items">{{ item | capitali\
10 zeWords }}</li></ul>`,
11     styles: [`
12         :host {
13             font-family: 'Arial';
14             display: flex;
15             width: 100%;
16             height: 100%;
17         }
18     `]
19 })
20 export class MyList implements OnInit {
21     items:Array<string>;
22     service:MyService;
23 
24     constructor(service:MyService) {
25         this.service = service;
26     }
27 
28     ngOnInit() {
29         this.items = this.service.getDogs(5);
30     }
31 }

Die vollständige Applikation zu diesem Kapitel ist auf der Github-Seite zum Buch zu finden.

Die Tests

Nun sind alle Voraussetzungen erfüllt. Die nötigen Bibliotheken stehen bereit und die zu testende Applikation ebenfalls.

Die Tests nennen sich hier Specs. Die Dateien, in denen der Testcode steht, sollten also in etwa den Aufbau name.spec.js haben, wenn sie in JavaScript erstellt wurde oder name.spec.ts in TypeScript. In der Konfigurationsdatei zu Karma wurde dies bereits vorweggenommen, weil das Dateimuster für die Suche der Tests *.spec.ts enthält. Hier also der erste Test:

src/comps/my-list.spec.ts
 1 import {it, describe, expect, beforeEach, inject} from '@angular/tes\
 2 ting';
 3 import {MyList} from "./my-list";
 4 import {MyService} from "../services/my-service";
 5 
 6 describe('MyList Tests', () => {
 7     let list:MyList;
 8     let service:MyService = new MyService();
 9 
10     beforeEach(() => {
11         list = new MyList(service);
12     });
13 
14     it('Should get 5 dogs', () => {
15         list.ngOnInit();
16 
17         expect(list.items.length).toBe(5);
18         expect(list.items).toEqual(['golden retriever', 'french bull\
19 dog', 'german shepherd', 'alaskan husky', 'jack russel terrier']);
20     });
21 });

Die Tests haben einen sehr einfachen und direkten Aufbau. Es beginnt mit einer kurze Beschreibung mittels describe – das ist für die Berichte wichtig. In der Lambda-Funktion erfolgt dann das Erstellen einer Instanz des Dienstes, der nhier als erstes getestet werden soll.

Der Befehl beforeEach wird immer vor jedem folgende Test ausgeführt. Hier wird eine Initalisierung vorgenommen.

Der Befehl it ist der eigentliche Test. Tests haben Testparameter und dann eine erwartetes Ergebnis und ein tatsächliches. Idealerweise sind diese gleich. Neben den Gleichheitstests gibt es auch solche, die auf bestimmte Werte wie Zahlen oder null prüfen. Hier wird toBe und toEqual benutzt. Es gibt viele weitere Testmethoden.

Listing: src/services/my-service.spec.ts
 1 import {it, describe, expect, beforeEach, inject} from '@angular/tes\
 2 ting';
 3 import {MyService} from "./my-service";
 4 
 5 describe('MyService Tests', () => {
 6     let service:MyService = new MyService();
 7 
 8     it('Should return a list of dogs', () => {
 9         var items = service.getDogs(4);
10 
11         expect(items).toEqual(['golden retriever', 'french bulldog',\
12  'german shepherd', 'alaskan husky']);
13     });
14 
15     it('Should get all dogs available', () => {
16         var items = service.getDogs(100);
17 
18         expect(items).toEqual(['golden retriever', 'french bulldog',\
19  'german shepherd', 'alaskan husky', 'jack russel terrier', 'boxer',\
20  'chow chow', 'pug', 'akita', 'corgi', 'labrador']);
21     });
22 });

Und nun die Filter.

src/pipes/my-pipe.spec.ts
 1 import {it, describe, expect, beforeEach, inject} from '@angular/tes\
 2 ting';
 3 import {MyPipe} from "./my-pipe";
 4 
 5 describe('MyPipe Tests', () => {
 6     let pipe:MyPipe;
 7 
 8     beforeEach(() => {
 9         pipe = new MyPipe();
10     });
11 
12     it('Should capitalize all words in a string', () => {
13         var result = pipe.transform('golden retriever', null);
14 
15         expect(result).toEqual('Golden Retriever');
16     });
17 });
Tests Ausführen

Sind Applikation und Tests fertig, können die Tests ausgeführt werden. Dies ist die Stunde der npm Kommandos. Zuerst der Erstellungsvorgang:

1 $ npm run build

angular2-testing@1.0.0 build /Applications/MAMP/htdocs/angular2-testing rm -rf dist && tsc -p src

Nun sollte ein Ordner dist erscheinen. Ist alles korrekt, werden die Tests ausgeführt;

1 $ npm test

angular2-testing@1.0.0 pretest /Applications/MAMP/htdocs/angular2-testing npm run build

angular2-testing@1.0.0 build /Applications/MAMP/htdocs/angular2-testing rm -rf dist && tsc -p src

angular2-testing@1.0.0 test /Applications/MAMP/htdocs/angular2-testing karma start karma.conf.js

22 01 2016 10:38:11.828:INFO [karma]: Karma v0.13.19 server started at http://localhost:9876/ 22 01 2016 10:38:11.836:INFO [launcher]: Starting browser Chrome 22 01 2016 10:38:13.488:INFO [Chrome 47.0.2526 (Mac OS X 10.11.2)]: Connected on socket /#WX3JgS5J8ql9at32AAAA with id 3078715 Chrome 47.0.2526 (Mac OS X 10.11.2): Executed 4 of 4 SUCCESS (0.018 secs / 0.005 secs) . Chrome 47.0.2526 (Mac OS X 10.11.2): Executed 4 of 4 SUCCESS (0.018 secs / 0.005 secs)

angular2-testing@1.0.0 posttest /Applications/MAMP/htdocs/angular2-testing remap-istanbul -i coverage/coverage-final.json -o coverage -t html

Das Ergebnis zeigt, das alles perfekt läuft. Aber wie sieht es mit der Testabdeckung aus?

Die Testabdeckung

Zuerst wird der Bericht erstellt:

1 $ npm run coverage

angular2-testing@1.0.0 coverage /Applications/MAMP/htdocs/angular2-testing http-server -c-1 -o -p 9875 ./coverage

Starting up http-server, serving ./coverage Available on: http:127.0.0.1:9875 http:192.168.1.38:9875 Hit CTRL-C to stop the server

Die Tests decken die wenigen Funktionen vollständig ab und so wird eine Testabdeckung von 100% erreicht. Der Report zeigt dies an.

1 $ npm start

angular2-testing@1.0.0 start /Applications/MAMP/htdocs/angular2-testing http-server -c-1 -o -p 8875 .

Starting up http-server, serving . Available on: http:127.0.0.1:8875 http:192.168.1.38:8875 Hit CTRL-C to stop the server

8.3 Die Serverumgebung

Im Kapitel zu dynamischen Webseiten wurde bereits ein sehr kompaktes Beispiel mit NodeJs gezeigt. Node spielt hier eine herausragende Rolle, um weitgehend unabhängig von Herstellern und Anbietern zu entwickeln. Sie müssen auch keine weitere Sprache wie Ruby, C#, Java oder Python lernen, um Server zu programmieren. JavaScript alleine reicht aus und mit TypeScript macht es mehr Spaß.

Deshalb an dieser Stelle eine kompakte Einführung in NodeJs.

NodeJS

Dieses Kapitel zeigt die wichtigstens Module, mit denen elementare Aufgaben in einer Webapplikation erledigt werden können. Es handelt sich dabei um die eigentliche Node-Bibliothek.

Globale Module

Globale Module sind immer vorhanden und müssen nicht vereinbart werden.

Timer

Zeitgeber abstrahieren weitgehend die von JavaScript standardmäßig angebotenen Möglichkeiten. Nutzen Sie unbedingt die node-Variante, um später keine Probleme mit anderen parallel laufenden Modulen zu bekommen.

Der Befehl setTimeout vereinbart den Aufruf der Rückruffunktkon nach einem bestimmten Zeitraum in Millisekunden. Optional können Argumente angegeben werden. Die Funktion gibt ein Objekt vom Typ timeoutObject zurück, dass mit clearTimeout() benutzt werden kann.

Syntax: setTimeout(callback, delay[, arg][, ...])

Die Funktion clearTimeout verhindert den Aufruf.

Syntax: clearTimeout(timeoutObject)

Auch die Funktion setInterval entspricht der internen JavaScript-Funktion, läuft aber unter Kontrolle von node ab. Die Rückruffunktion wird wiederholt nach Ablauf des Intervalls aufgerufen. Die Funktion gibt ein Objekt vom Typ intervalObject zurück, dass mit clearInterval() benutzt werden kann.

Syntax: setInterval(callback, delay[, arg][, ...])

Die Funktion clearInterval stoppt den wiederholten Aufruf.

Syntax: clearInterval(intervalObject)

Die Methode unref wird von den Objekten timeoutObject und intervalObject angeboten. Wenn eine node-Applikation endet, und sich noch Zeitgeber in Aktion befinden, wird die Ausführung dennoch fortgesetzt, bis der letzte Zeitgeber abgelaufen ist. Mit unref kann angezeigt werden, dass die Beendigung der Applikation auch die übriggebliebenen Zeitgeber stoppt und nicht weiter ausführt. Der mehrfache Aufruf von unref auf demselben Objekt hat keinen Effekt.

Die Funktion verschiebt den Zeitgeber in die Hauptschleife der Applikation. Zuviele solche Zeitgeber können die Leistung der Hauptschleife beeinflussen. Sie sollten unref daher bewusst und nur falls unbedingt notwendig einsetzen.

Ein zuvor mit unref in die Hauptschleife verschobener Zeitgeber kann mit der Funktion ref wieder in seinen regulären Zustand überführt werden. Der mehrfache Aufruf hat keine Effekt.

Die Methode setImmediate ist ein höher priorisierter Zeitgeber, der nach I/O-Ereignissen auslöst und vor setTimeout und vor setInterval aufgerufen wird. Dieser Zeitgeber gibt ein Objekt immediateObject zurück, das mit clearImmediate() benutzt werden kann. Mehrere Rückruffunktionen werden in einer Warteschlange platziert und in der Reihenfolge abgearbeitet, wie sie definiert wurden. Die Ausführung der Warteschlange erfolgt einmal pro Durchlauf der Hauptschleife der Applikation. Ein neu platziertes Objekt wird also erst dann ausgeführt, wenn die Hauptschleife das nächste Mal durchläuft.

Syntax: setImmediate(callback[, arg][, ...])

clearImmediate stoppt die Ausführung des mit immediateObject bezeichneten Zeitgebers.

Syntax: clearImmediate(immediateObject)

Globale Objekte

Globale Objekte sind in allen Modulen aktiv. Sie müssen nicht separat vereinbart werden.

Mit global ist der globale Namensraum gemeint. Eine Variable in JavaScript ist im globalen Namensraum, auch wenn sie mit var definiert wurde, global. In Node ist dies nicht der Fall – der “globale” Namensraum ist immer das aktuelle Modul. Erst durch den expliziten Zugriff auf global wird ein globaler Namensraum möglich.

Das process-Objekt zeigt Informationen zum Prozess an. Mit console diesem Objekt besteht Zugriff auf die Konsole. Das Buffer-Objekt beinhaltet den Umgang mit gepufferten Daten.

Die Funktion requirefordert ein Modul an. Diese Funktion ist nicht wirklich global, sondern wird automatisch in jedem Modul lokal vereinbart, sodass sie wie eine globale Funktion immer verfügbar ist.

Die Methode require.resolve nutzt den Suchmechanismus für Module, lädt aber im Erfolgsfall das Modul nicht, sondern gibt lediglich den Pfad zurück, unter dem des gefunden wurde. Module können lokal oder global installiert sein, sodass der Fundort durchaus variiert. Mit require.cache werden Module in dem Objekt, dass diese Eigenschaft zurückgibt, gecacht. Wenn das Modul aus dem Cache durch Löschen des Schlüssels entfernt wird, wird der nächste Aufruf von require das Modul erneut laden.

__filename ist der Dateiname der aktuell ausgeführten Code-Datei. Der Name enthält den aufgelösten, absoluten Pfad. Dies muss nicht derselben Pfad sein wie er mit den Kommandozeilenwerkzeugen benutzt wird. Wenn der Aufruf in einem Modul erfolgt, ist das Modul die ausgeführte Code-Datei und der Pfad zeigt zum Modul.

Wenn beispielsweise die Datei example.js im Pfad /User/joerg/Apps ausgeführt wird, gibt der folgende Aufruf /User/joerg/Apps/example.js zurück:

console.log(__filename);

__filename ist global nutzbar, wird aber in jedem Modul lokal definiert.

__dirname ist das Verzeichnis, in dem die aktuell ausgeführte Datei ist.

Wenn beispielsweise die Datei example.js im Pfad /User/joerg/Apps ausgeführt wird, gibt der folgende Aufruf /User/joerg/Apps zurück:

console.log(__dirname);

__dirname ist global nutzbar, wird aber in jedem Modul lokal definiert.

module ist eine Referenz zum aktuelle Modul. Die Eigenschaft module.exports wird dazu benutzt, die vom Modul exportierten Funktionen bereitzustellen. Verfügbar gemacht werden diese durch den Aufruf von require().

module ist global nutzbar, wird aber in jedem Modul lokal definiert.

exports ist ein Alias für module.exports und verkürzt lediglich den Schreibaufwand. exports ist global nutzbar, wird aber in jedem Modul lokal definiert.

HTTP und HTTPS

Mit den http- bzw. https-Modulen werden nahezu alle Fassetten der Protokolle HTTP und HTTPS unterstützt. Die Kommunikation auf dieser Ebene ist sehr elementar. Frameworks wie Express abstrahieren dies und setzen selbst auf http auf. Trotzdem kann es für viele Fälle sinnvoll sein, Protokollaktionen direkt auszuführen.

Node kann mit Streams umgehen – also einem fortlaufenden Strom von Bytes. Dies ist weit effektiver als die gesamten Daten für einen Vorgang im Speicher zu halten (Puffer, buffering). Die http-Module kümmern sich um das Verarbeiten von Daten mit Streams und erleichtern erheblich die Programmierung.

Grundlagen

HTTP besteht aus einer Befehlszeile und Kopffeldern, die den Befehl näher beschreiben. In Node werden die Kopffelder als JSON bereitgestellt. Ein entsprechendes Objekt könnte also folgendermaßen aussehen:

1 { 
2   'content-length': '123',
3   'content-type': 'text/plain',
4   'connection': 'keep-alive',
5   'host': 'mysite.com',
6   'accept': '*/*' 
7 }

Die Schlüssel werden entsprechend der Spezifikation immer in Kleinbuchstaben konvertiert. Die Werte werden dagegen niemals verändert. Das ist bereits der ganze Eingriff von Node an dieser Stelle. Generell ist Node bei diesem Modul sehr einfach. Weder die Kopffelder noch der Inhalt einer Nachricht werden untersucht, bewertet oder intern behandelt.

Kopffelder, die mehrere Werte haben, nutzen das , (Komma) zum Trennen der Werte. Einzige Ausnahme sind die Kopffelder für Cookies, die ein Array akzeptieren. Wenn Felder nur einen Wert erlauben, kontrolliert Node dies und wirft eine Ausnahme.

Eintreffende oder gesendete Kopffelder werden als unbearbeitetes Objekt bereitgestellt. Dies ist ein Array mit fortlaufenden Paaren von Schlüsseln und Werten. Das sieht etwa folgendermaßen aus:

1 [ 'Content-Length', '123456',
2   'content-type', 'text/plain',
3   'CONNECTION', 'keep-alive',
4   'Host', 'mysite.com',
5   'accept', '*/*' ]

Umwandlungs- und Kontrollaktionen finden danach statt, sodass die tatsächlich bereitgestellten oder gesendeten Kopffelder davon abweichen können.

Felder

Dieser Abschnitt beschreibt Felder, die Werte bereitstellen, die auf die interne Konfiguration verweisen.

http.METHODS gibt in Form eines Arrays eine Liste der HTTP-Verben zurück, die unterstützt werden. http.STATUS_CODES ist ein Array mit den Statuscodes, die HTTP kennt, und dem zugeordneten Kurztext. Für 404 ist dies beispielhaft wie folgt definiert:

http.STATUS_CODES[404] === 'Not Found'

Methoden

Die Methoden ermöglichen die entsprechenden Aktionen in Bezug auf die Protokollverarbeitung. http.createServer gibt eine neue Instanz des Http-Servers zurück. Damit können HTTP-Anfragen empfangen und verarbeitet werden. Die Syntax sieht folgendermaßen aus:

http.createServer([requestListener])

Die Rückruffunktion requestListener ist eine Methode, die die empfangenen Daten bekommt.

Mit http.request(options[, callback]) sendet Node eine Anforderung (request) an einen anderen Server. Node ist also in diesem Fall der Client. Node benutzt mehrere Verbindungen, wenn dies möglich ist. Die Methode behandelt dies jedoch intern, sodass Sie beim Programmieren darauf keine Rücksicht nehmen müssen. Folgende Syntax wird benutzt:

http.request(options [, callback])

Die Optionen können JSON oder eine Zeichenkette sein. Ist es eine Zeichenkette, wird automatisch url.parse() eingesetzt, um die Zeichenkette zu parsen. Die Rückrufmethode liefert ein Objekt mit der Antwort (response).

Die Optionen haben folgende Bedeutung:

Die Methode selbst gibt eine Instant der Klasse http.ClientRequest zurück. Dies ist ein schreibbarer Stream. Werden für die Anfrage Daten benötigt, beispielsweise weil bei einer POST-Anforderung ein Formular gesendet wird, dann werden diese Daten in diesen Stream geschrieben.

 1 var postData = querystring.stringify({
 2   'msg' : 'Hello World!'
 3 });
 4 
 5 var options = {
 6   hostname: 'www.google.com',
 7   port: 80,
 8   path: '/upload',
 9   method: 'POST',
10   headers: {
11     'Content-Type': 'application/x-www-form-urlencoded',
12     'Content-Length': postData.length
13   }
14 };
15 
16 var req = http.request(options, function(res) {
17   console.log('STATUS: ' + res.statusCode);
18   console.log('HEADERS: ' + JSON.stringify(res.headers));
19   res.setEncoding('utf8');
20   res.on('data', function (chunk) {
21     console.log('BODY: ' + chunk);
22   });
23 });
24 
25 req.on('error', function(e) {
26   console.log('problem with request: ' + e.message);
27 });
28 
29 req.write(postData);
30 req.end();

Das eigentliche Schreiben erfolgt mit req.write(postData). Die Benutzung von req.end() ist hier notwendig, weil der Stream sonst nicht geschlossen wird. Nach dem Beenden können keine weiteren Daten geschrieben werden. Das Anforderungsobjekt req kennt ein Ereignis error, auf das Sie reagieren können, um Fehler abzufangen. Fehler können auftreten, wenn einer der Vorgänge beim Senden misslingt (DNS-Auflösung, TCP-Fehler, Fehler beim Parsen der Kopffelder usw.).

Wird das Kopffeld Connection: keep-alive manuell eingefügt, erkennt Node dies und hält die Verbindung offen, bis die nächste Anfrage gesendet wird.

Wird das Kopffeld Content-length gesendet, dann wird die Benutzung von Chunks abgeschaltet. Chunks sind das blockweise Senden von Daten. Die Angabe erfolgt durch das Kopffeld Transfer-Encoding: chunked.

Wird ein Expect-Kopffeld benutzt, dann werden die Kopffelder sofort gesendet. Nach Expect: 100-continue sollten Sie sofort auf das entsprechende Ereignis lauschen (mit Timeout). RFC2616 Section 8.2.3 gibt dazu mehr Informationen.

Wenn das Kopffeld Authorization angegeben wird, werden die durch die Option auth erzeugten Daten überschrieben.

Mit http.get steht eine verkürzte Variante der Methode request bereit, die eine Anfrage mittels ‘GET’ initiiert. Das bei GET keine Daten gesendet werden, wird req.end() automatisch erzeugt:

http.get(options[, callback])

Ein Beispiel zeigt, wie es geht:

1 http.get("http://www.google.com/index.html", function(res) {
2   console.log("Got response: " + res.statusCode);
3 }).on('error', function(e) {
4   console.log("Got error: " + e.message);
5 });
Klassen

Einige Klassen liefern weitere Funktionalität.

http.Server

Der HTTP-Server bietet eine Umgebung, die auf Aktionen des Protokolls mittels Ereignisse reagiert. Die Ereignisse sind:

Die Ereignisse werden über die Methode on erreicht:

1 var http = require("http");
2 var server = http.createServer();
3  
4 server.on("request", function (req, res) {
5     res.end("this is the response");
6 });
7  
8 server.listen(3000);
Methoden für http.Server

Das Objekt server selbst, das createServer erzeugt, verfügt über einige Methoden, die ebenfalls interessant sind.

Mit server.listen beginnt der Server am angegebenen Port und der entsprechenden Adresse – also dem Socket – zu lauschen. Wird der Hostname nicht angegeben, wird an allen IP-Adressen auf der Maschine gelaucht (nur IPv4). Foldende Varianten gibt es:

server.listen(port[, hostname][, backlog][, callback])

server.listen(path[, callback])

server.listen(handle[, callback])

Der Parameter backlog ist die Länge der Pufferwarteschlange für eintreffende Verbindungen. Wenn ein Verbindungswunsch eintrifft und der Vorgehende sich noch in Verarbeitung befindet, dann nimmt Node diese Anfrage in diese Warteschlange auf. Der Standardwert ist 511 (!sic). Werte bis 1000 sind hier sinnvoll. Zulange Warteschlangen suggerieren Clients, dass eine Verbindung zu erwarten ist, während Node kaum in der Lage ist, diese auch zu verarbeiten.

Wird ein handle benutzt, so ist dies ein Objekt, dass einen Server oder ein Socket beschreibt.

Die Funktion ist asynchron und arbeitet mit der Rückrufmethode callback.

Mit server.close stoppt der Server das Akzeptieren von Verbindungswünschen:

server.close([callback])

Da Verbindungen möglicherweise nicht zur Verfügung stehen, kann mit server.setTimeout ein Wert gesetzt werden, der bestimmt, wielange gewartet wird:

server.setTimeout(msecs, callback)

Der Wert wird in Millisekunden angegeben. Der Standardwert beträgt 2 Minuten. server.timeout gibt den gesetzten Wert wieder.

Mit server.maxHeadersCount wird die Anzahl der Kopffelder begrenzt. Standardmäßig sind dies 1000, mit 0 ist der Wert unbegrenzt.

Die Klasse http.ServerResponse

Eine Instanz dieser Klasse wird intern erstellt. Dies ist der Typ, der durch den Parameter response in der Rückruffunktion des Ereignisses request übergeben wird. Dies ist das Antwort-Objekt. Es implementiert einen schreibbaren Stream. Dieser arbeitet mit Ereignissen.

‘close’: function () { }
Zeigt an, dass die Verbindung geschlossen wurde, bevor end in der Lage war, die Daten zu senden. ‘finish’: function () { }
Wird ausgelöst, wenn die Übertragung der Antwort erledigt ist. Für Node ist dies der Moment der Übergabe an das Betriebssystem. Das heißt nicht zwingend, dass die Daten den Computer verlassen haben oder der Client diese Daten empfangen hat.

Auf eine Instanz dieser Klasse sind vielfältige Operationen möglich. response.writeContinue sendet ein HTTP/1.1 100 Continue an den Client um ihn aufzufordern, dass die Daten gesendet werden können. Mit response.writeHead sendet Node den Kopf – Statuscode plus Kopffelder – an den Client. Der Statuscode ist der dreistellige HTTP-Code, beispielsweise 200 oder 404. Die Kopffelder können passend dazu angegeben werden. Folgende Syntax ist anwendbar:

response.writeHead(statusCode[, statusMessage][, headers])

1 var body = 'hello world';
2 response.writeHead(200, {
3   'Content-Length': body.length,
4   'Content-Type': 'text/plain' }
5 );

Diese Methode darf nur einmal aufgerufen werden und dies muss vor response.end() erfolgen.

Alternativ kann mit response.write() und response.end() gearbeitet werden. Wird response.write() benutzt und ist die Antwort noch nicht beendet worden, berechnet Node beim Aufruf von writeHead die kumulierten Kopffelder.

Mit response.setTimeout wird der Timeout-Wert in Millisekunden gesetzt:

response.setTimeout(msecs, callback)

Die Rückruffunktion callback wird aufgerufen, wenn die Zeit abläuft. Wenn keine Angabe erfolgt, werden nach Ablauf der Zeit die entsprechenden Objekte für Socket, Server, Response usw. aufgeräumt. Ist aber eine Rückruffunktion vorhanden, so müssen Sie dies in dieser Funktion selbst erledigen.

Mit response.statusCode legen Sie fest, welcher Statuscode benutzt wird. Dies ist nicht notwendig, wenn Sie mit writeHead arbeiten.

response.statusCode = 404;

Die Eigenschaft enthält den tatsächlichen Wert nach dem Senden der Antwort.

Legen Sie mit response.statusMessage dieser Eigenschaft fest, welcher Statuscode benutzt wird. Dies ist nicht notwendig, wenn Sie mit writeHead arbeiten. Die Angabe ist nur sinnvoll, wenn Sie etwas anderes als den Standardtext senden wollen.

response.statusMessage = 'Not found';

Die Eigenschaft enthält den tatsächlichen Wert nach dem Senden der Antwort.

response.setHeader erzeugt ein Kopffeld oder ersetzt ein Kopffeld, falls es schon vorhanden ist in der Liste der zu sendenden Kopffelder. Sollen mehrere Kopffelder erzeugt werden, können Sie ein Array benutzen. Folgende Syntax gilt:

response.setHeader(name, value)

1 response.setHeader("Content-Type", "text/html");
2 response.setHeader("Set-Cookie", ["type=ninja", "language=javascript\
3 "]);

Jeder kann mit response.headersSent ermittelt werden, ob die Kopffelder bereits gesendet worden sind.

response.sendDate ist eine Boolesche Eigenschaft die anzeigt, ob das Kopffeld Date erzeugt werden soll. Sollte dieses Kopffeld bereits manuell eingetragen sein, wird der manuelle Eintrag nicht überschrieben.

response.getHeader list ein Kopffeld, solange es noch nicht gesendet wurde. Nach dem Senden ist kein Zugriff mehr möglich. Der Name berücksichtigt Groß- und Kleinschreibung nicht – intern sind alle Kopffeldnamen kleingeschrieben. Die Syntax dieser Methode ist wie folgt:

response.getHeader(name)

1 var contentType = response.getHeader('content-type');

response.removeHeader entfernt ein Kopffeld, solange es noch nicht gesendet wurde:

1 response.removeHeader("Content-Encoding");

Die Methode response.write schreibt eine Menge an Daten. Das führt dazu, dass implizit festgelegte Kopffelder gesendet werden, weil diese vor den Daten übertragen werden müssen. Wenn zuvor response.writeHead() benutzt wurde, werden die dort definierten Kopffelder benutzt.

response.write(chunk[, encoding][, callback])

Die Methode kann mehrfach aufgerufen werden, um Daten blockweise (chunks) zu übertragen. Der Parameter chunk kann eine Zeichenkette oder ein Byte-Stream sein. Wenn die Daten eine Zeichenkette sind, bestimmt der Parameter encoding, wie diese in Bytes konvertiert werden. Der Standardwert ist ‘utf-8’. Die Rückrufmethode callback wird aufgerufen, wenn die Daten gesendet wurden.

Die Methode gibt true zurück, wenn die Daten an den internen Puffer übergeben wurden. false wird zurückgegeben, wenn Daten im Speicher verblieben sind. ‘drain’ wird erzeugt, wenn der Puffer wieder leer ist.

Mit response.addTrailers(headers) werden Kopffelder an das Ende der Nachricht angehängt. Das geht nur bei Daten, die in Chunks geliefert werden.

1 response.writeHead(200, { 'Content-Type': 'text/plain',
2                           'Trailer': 'Content-MD5' });
3 response.write(fileData);
4 response.addTrailers({
5    'Content-MD5': "7895bf4b8828b55ceaf47747b4bca667"
6 });
7 response.end();

Mit response.end wird mitgeteilt, dass die Übertragung beendet wird. Diese Methode muss immer aufgerufen werden.

response.end([data][, encoding][, callback])

Wenn Daten angegeben sind, wird intern response.write(data, encoding) aufgerufen. Die Rückruffunktion wird aufgerufen, wenn alle Daten gesendet worden sind.

Klasse http.ClientRequest

Eine Instanz dieser Klasse wird durch http.request() erstellt. Dies ist das Anforderungsobjekt. Die Kopffelder sind danach noch änderbar mit den Methoden setHeader(name, value), getHeader(name) und removeHeader(name). Node ist in diesem Fall der Client, der Anfragen an einen anderen Server sendet.

Um die Antwort auf die erzeugte und gesendete Anfrage zu bekommen, übergeben Sie eine Ereignisbehandlungsfunktion für das Ereignis response. Das Ereignis gibt eine Instanz der Klasse IncomingMessage zurück. Sollte die Antwort Daten enthalten, kann mit dem Ereignis data darauf zugegriffen werden. Alternativ kann auf das Ereignis readable gelauscht werden und dann werden die Daten aktiv mit response.read() gelesen.

 1 var http = require('http');
 2 var net = require('net');
 3 var url = require('url');
 4 
 5 // Proxy für Tunnel erstellen
 6 var proxy = http.createServer(function (req, res) {
 7   res.writeHead(200, {'Content-Type': 'text/plain'});
 8   res.end('okay');
 9 });
10 proxy.on('connect', function(req, cltSocket, head) {
11   // Ursprünglichen Server verbinden
12   var srvUrl = url.parse('http://' + req.url);
13   var srvSocket = net.connect(srvUrl.port, srvUrl.hostname, 
14                               function() {
15     cltSocket.write('HTTP/1.1 200 Connection Established\r\n' +
16                     'Proxy-agent: Node-Proxy\r\n' +
17                     '\r\n');
18     srvSocket.write(head);
19     srvSocket.pipe(cltSocket);
20     cltSocket.pipe(srvSocket);
21   }); // Ende function
22 });
23 
24 // Proxy läuft jetzt
25 proxy.listen(1337, '127.0.0.1', function() {
26 
27   // Anforderung erstellen
28   var options = {
29     port: 1337,
30     hostname: '127.0.0.1',
31     method: 'CONNECT',
32     path: 'www.google.com:80'
33   };
34 
35   var req = http.request(options);
36   req.end();
37 
38   req.on('connect', function(res, socket, head) {
39     console.log('got connected!');
40 
41     // Anforderung über Tunnel
42     socket.write('GET / HTTP/1.1\r\n' +
43                  'Host: www.google.com:80\r\n' +
44                  'Connection: close\r\n' +
45                  '\r\n');
46     socket.on('data', function(chunk) {
47       console.log(chunk.toString());
48     });
49     socket.on('end', function() {
50       proxy.close();
51     });
52   });
53 });

Ein weiteres Ereignis muss gegebenenfalls behandelt werden: upgrade. Die Rückruffunktion hat folgende Signatur:

function (response, socket, head)

Ein Upgrade ist erforderlich, wenn der Client das Protokoll wechseln möchte, beispielsweise von HTTP 1.1 auf HTTP 2.0 oder auf WebSockets.

 1 var http = require('http');
 2 
 3 // Create an HTTP server
 4 var srv = http.createServer(function (req, res) {
 5   res.writeHead(200, {'Content-Type': 'text/plain'});
 6   res.end('okay');
 7 });
 8 srv.on('upgrade', function(req, socket, head) {
 9   socket.write('HTTP/1.1 101 Web Socket Protocol Handshake\r\n' +
10                'Upgrade: WebSocket\r\n' +
11                'Connection: Upgrade\r\n' +
12                '\r\n');
13 
14   socket.pipe(socket); // Echo zurück
15 });
16 
17 // now that server is running
18 srv.listen(1337, '127.0.0.1', function() {
19 
20   // make a request
21   var options = {
22     port: 1337,
23     hostname: '127.0.0.1',
24     headers: {
25       'Connection': 'Upgrade',
26       'Upgrade': 'websocket'
27     }
28   };
29 
30   var req = http.request(options);
31   req.end();
32 
33   req.on('upgrade', function(res, socket, upgradeHead) {
34     console.log('got upgraded!');
35     socket.end();
36     process.exit(0);
37   });
38 });

Das Ereignis continue tritt auf, wenn der Server ein 100 Continue sendet, was meist eine Reaktion auf die Anforderung Expect: 100-continue ist. Dies ist die Aufforderung für den Client, dass die Daten der Nachricht gesendet werden dürfen.

Mit request.flushHeaders() steht eine Methode zur Verfügung, die die Kopffelder aktiv sendet. Normalerweise puffert Node Kopffelder und sendet diese nicht sofort, wenn sie definiert werden. Die Pufferung dient der Optimierung, sodass alle Kopffelder idealerweise in ein TCP-Paket passen. Mit flush() und flushHeaders() wird der Optimierungsmechanismus übergangen.

Das eigentlichen Schreiben der Daten erledigt request.write(chunk[, encoding][, callback]) durch blockweises (chunk) Senden der Daten. Es sollte das Kopffeld [‘Transfer-Encoding’, ‘chunked’] benutzt werden, um der Gegenstelle anzuzeigen, dass mit Blöcken gearbeitet wird.

Das Argument chunk kann ein Buffer oder eine Zeichenkette sein. Die Rückruffunktion wird aufgerufen, wenn die Daten gesendet worden sind.

Mit request.end([data][, encoding][, callback]) wird die Anforderung beendet. Wenn Teile der Daten noch nicht gesendet worden sind, wird ein flush erzwungen. Wurden Blöcke benutzt, wird nun die finale Sequenz ‘0\r\n\r\n’ gesendet.

Mit Daten ist das Ergebnis identisch mit dem Aufruf von request.write(data, encoding), gefolgt von request.end(callback). Die Rückruffunktion wird aufgerufen, wenn die Daten gesendet worden sind.

Mit request.abort() kann die Anforderung abgebrochen werden. Mit request.setTimeout(timeout[, callback]) wird der Timeout-Wert festgelegt.

http.IncomingMessage

Eine eintreffende Nachricht vom Typ IncomingMessage wird durch http.Server oder http.ClientRequest erzeugt. Das Objekt wird als erstes Argument des request- bzw. response-Ereignisses übergeben. Das Objekt implementiert einen lesbaren Stream sowie einige weitere Methoden und Eigenschaften.

Mit dem Ereignise close wird angezeigt, dass die Verbindung geschlossen wurde. Dieses Ereignis kann nur einmal auftreten.

Die Eigenschaft message.httpVersion zeigt an, welche HTTP-Version benutzt wurde. Das ist entweder ‘1.1’ oder ‘1.0’ usw. Zum Zugriff auf die Versionsdetails dienen response.httpVersionMajor und response.httpVersionMinor.

Die Kopffelder lassen sich über message.headers auslesen. Kopffelder sind intern immer mit Kleinbuchstaben bezeichnet. Die Ausgabe mittel console.log(request.headers); erzeugt etwa folgendes JSON-Objekt:

1 { 
2   'user-agent': 'curl/7.22.0',
3   host: '127.0.0.1:8000',
4   accept: '*/*' 
5 }

Wenn Sie die Kopffelder direkt, ohne die Behandlung innerhalb von Node, lesen möchten, eignet sich message.rawHeaders. Interessant hier ist, dass dies kein Verzeichnis mit Schlüssel-/Wertepaaren ist, sondern ein Array, indem abwechseln die Kopffelder und deren Werte stehen.

 1 [ 
 2   'user-agent',
 3   'this is invalid because there can be only one',
 4   'User-Agent',
 5   'curl/7.22.0',
 6   'Host',
 7   '127.0.0.1:8000',
 8   'ACCEPT',
 9   '*/*' 
10 ]

Im end-Ereignis (und nur dort) lassen sich mit message.trailers und message.rawTrailers die Trailer einer in Blöcken (chunks) übertragenen Nachricht abfragen. Mittels Trailer werden blockweise übertragene Nachrichten korrekt zusammengesetzt.

Eine zeitliche Begrenzung der Verarbeitung der Nachricht kann mit message.setTimeout(msecs, callback) erreicht werden. Die Angabe der Zeit erfolgt in Millisekunden, nach Ablauf wird callback aufgerufen.

Das benutzte HTTP-Verb kann der Eigenschaft message.method entnommen werden. In message.url steht der URL der Anforderung. Diese Eigenschaften funktionieren nur, wenn das Objekt von http.Server stammt. Folgender Anforderung soll als Beispiel dienen:

1 GET /status?name=ryan HTTP/1.1\r\n
2 Accept: text/plain\r\n
3 \r\n

In request.url steht dann: ‘/status?name=ryan’

Zum Verarbeiten des URL dient parse:

1 var url = require('url');
2 console.log(url.parse('/status?name=ryan'));

Folgende Ausgabe wird erzeugt:

1 { 
2   href: '/status?name=ryan',
3   search: '?name=ryan',
4   query: 'name=ryan',
5   pathname: '/status' 
6 }

Die Verarbeitung des Querystring kann in einem weiteren Schritt erfolgen:

1 var url = require('url');
2 console.log(url.parse('/status?name=ryan', true));

Folgende Ausgabe wird erzeugt:

1 { 
2   href: '/status?name=ryan',
3   search: '?name=ryan',
4   query: { name: 'ryan' },
5   pathname: '/status' 
6 }

Der Statuscode, der bei der Beantwortung der Nachricht benutzt wird, steht in message.statusCode, der passenden Text dazu in message.statusMessage. Der Code ist der dreistellige HTTP-Code, z.B. 404. Dieser Wert ist nur ereichbar, wenn das Objekt von http.ClientRequest stammt.

Mittels message.socket besteht Zugriff auf das net.Socket-Objekt, dass der benutzten Verbindung zugeordnet ist.

HTTPS

HTTPS ist HTTP, das auf TLS (Transport Layer Security) aufsetzt. Die aktuelle TLS-Version entspricht dem früheren Standard SSL 3.0, TLS ist der Nachfolger von SSL.

Wird HTTPS benutzt, so können Sie mit request.connection.verifyPeer() und request.connection.getPeerCertificate() die Authentifizierungsdaten des Clients ermitteln.

Der Server wird wie bei HTTP folgendermaßen erstellt:

https.createServer(options[, requestListener])

 1 // Abruf: https://localhost:8000/
 2 var https = require('https');
 3 var fs = require('fs');
 4 
 5 var options = {
 6   key: fs.readFileSync('test/fixtures/keys/agent2-key.pem'),
 7   cert: fs.readFileSync('test/fixtures/keys/agent2-cert.pem')
 8 };
 9 
10 https.createServer(options, function (req, res) {
11   res.writeHead(200);
12   res.end("hello world\n");
13 }).listen(8000);
14 Or
15 
16 var https = require('https');
17 var fs = require('fs');
18 
19 var options = {
20   pfx: fs.readFileSync('server.pfx')
21 };
22 
23 https.createServer(options, function (req, res) {
24   res.writeHead(200);
25   res.end("hello world\n");
26 }).listen(8000);

Die benutzten Methoden und Eigenschaften gleichen weitgehend denen des Moduls ‘http’.

 1 var https = require('https');
 2 
 3 var options = {
 4   hostname: 'encrypted.google.com',
 5   port: 443,
 6   path: '/',
 7   method: 'GET'
 8 };
 9 
10 var req = https.request(options, function(res) {
11   console.log("statusCode: ", res.statusCode);
12   console.log("headers: ", res.headers);
13 
14   res.on('data', function(d) {
15     process.stdout.write(d);
16   });
17 });
18 req.end();
19 
20 req.on('error', function(e) {
21   console.error(e);
22 });

Das Argument options hat gegenüber ‘http’ weitere Optionen:

 1 var options = {
 2   hostname: 'encrypted.google.com',
 3   port: 443,
 4   path: '/',
 5   method: 'GET',
 6   key: fs.readFileSync('test/fixtures/keys/agent2-key.pem'),
 7   cert: fs.readFileSync('test/fixtures/keys/agent2-cert.pem')
 8 };
 9 options.agent = new https.Agent(options);
10 
11 var req = https.request(options, function(res) {
12   ...
13 }

Sie können dies auch ohne Agent-Objekt nutzen:

 1 var options = {
 2   hostname: 'encrypted.google.com',
 3   port: 443,
 4   path: '/',
 5   method: 'GET',
 6   key: fs.readFileSync('test/fixtures/keys/agent2-key.pem'),
 7   cert: fs.readFileSync('test/fixtures/keys/agent2-cert.pem'),
 8   agent: false
 9 };
10 
11 var req = https.request(options, function(res) {
12   ...
13 }

Umgang mit Dateien und Pfaden

Node kann über die entsprechenden Module direkt auf Dateien zugreifen und alle typischen Operationen auf diesen Dateien sowie auf Pfaden und Ordnern ausführen.

Zugriff auf das Dateisystem

Der Dateisystemzugriff unter Node erfolgt über das Modul fs. Alle Aufrufe können sowohl synchron als auch asynchron erfolgen. Während für clientseitige Skripte grundsätzlich nur asynchrone Aufrufe sinnvoll sind, kann dies auf dem Server etwas anders betrachtet werden. Da das Ergebnis einer Aktion möglicherweise das Aussenden von JSON oder HTML ist, wird in der Regel ohnehin gewartet, bis das Ergebnis vorliegt. Dabei haben asynchrone Aufrufe keinen Vorteil. Wenn Ihre Umgebung allerdings stark belastet ist und Skripte spürbare Laufzeiten haben, so wird Node nur immer eine Anfrage bearbeiten und dann alle synchronen Aktionen für diese ausführen. Alle anderen Anfragen warten solange. Wartet nun seinerseits ein Skript erheblich auf eine Dateioperation, so wird der Prozess insgesamt verlangsamt.

Asynchrone Aufrufe nutzen immer eine Rückruffunktion als letztes Argument. Die Rückruffunktionen haben verschiedene Signaturen. Gemeinsam ist jedoch allen Funktionen, dass das erste Argument der Rückruffunktion ein Ausnahmeobjekt (exception) ist, das Fehler anzeigt. Im Erfolgsfall ist dieses Objekt undefined oder null, sodass ein einfacher Test mit if(!exception) auf Erfolg testet.

Synchrone Aufrufe erzeugen immer sofort eine Ausnahme, wenn ein Fehler auftritt. nutzen Sie hier try/catch zum Behandeln der Fehlerzustände.

Hier ein erstes Beispiel für die asynchrone Nutzung:

1 var fs = require('fs');
2 
3 fs.unlink('/tmp/hello', function (err) {
4   if (err) throw err;
5   console.log('successfully deleted /tmp/hello');
6 });

Hier ein dasselbe Beispiel für die synchrone Nutzung (beachten Sie den Suffix Sync in Zeile 3):

1 var fs = require('fs');
2 
3 fs.unlinkSync('/tmp/hello');
4 console.log('successfully deleted /tmp/hello');

Asynchrone Aufrufe kehren zu einem nicht deterministischen Zeitpunkt zurück. Wenn Sie mehrere Aufrufe starten, ist die Reihenfolge bei der Rückkehr nicht garantiert. Das folgende Beispiel ist deshalb fehleranfällig:

1 fs.rename('/tmp/hello', '/tmp/world', function (err) {
2   if (err) throw err;
3   console.log('renamed complete');
4 });
5 fs.stat('/tmp/world', function (err, stats) {
6   if (err) throw err;
7   console.log('stats: ' + JSON.stringify(stats));
8 });

Es kann hier passieren, dass der Aufruf von fs.stat in Zeile 5 erfolgt, bevor bei der vorherigen Aktion in Zeile 1 das umbenennen mit fs.rename abgeschlossen wurde. Deshalb sollten Sie mehrere aufeinander aufbauende asynchrone Aufrufe verketten:

1 fs.rename('/tmp/hello', '/tmp/world', function (err) {
2   if (err) throw err;
3   fs.stat('/tmp/world', function (err, stats) {
4     if (err) throw err;
5     console.log('stats: ' + JSON.stringify(stats));
6   });
7 });

Sie können beim Aufruf mit absoluten oder relativen Pfaden arbeiten. Wenn Sie mit relativen Pfaden arbeiten, sollte klar sein, dass der Ursprung des aktuellen Verzeichnisses der Prozess ist, in dem das Skript ausgeführt wird. Dies kann mit process.cwd() bestimmt werden. In der Regel ist dies der Node-Core.

Machmal kann es vorkommen, dass Sie die Aktion zwar ausführen, das Ergebnis aber nicht benötigen. Dann können Sie die Rückruffunktion weglassen. Tritt nun aber ein Fehler auf, fehlt der Zugang zum Ausnahmeobjekt. Um dennoch an diese Fehlermeldung zu gelangen, nutzen Sie die Umgebungsvariable NODE_DEBUG. Das folgende Skript zeigt, wie das erfolgt:

Datei: script.js
1 function bad() {
2   require('fs').readFile('/');
3 }
4 bad();

Führen Sie das Skript nun folgendermaßen aus:

1 $ env NODE_DEBUG=fs node script.js

Es erfolgt folgende Ausgabe:

 1 fs.js:66
 2         throw err;
 3               ^
 4 Error: EISDIR, read
 5     at rethrow (fs.js:61:21)
 6     at maybeCallback (fs.js:79:42)
 7     at Object.fs.readFile (fs.js:153:18)
 8     at bad (/path/to/script.js:2:17)
 9     at Object.<anonymous> (/path/to/script.js:5:1)
10     <etc.>

Dies gelingt freilich nur, wenn der Pfad wirklich nicht gelesen werden kann – in dem Beispiel wird auf die Root ‘/’ zugegriffen.

Funktionen für den Dateizugriff

Dieser Abschnitt zeigt die wichtigsten Dateizugriffsfunktionen. Hier werden nur die asynchronen Methoden gezeigt. Die meisten Methoden existieren auch synchron. Sie haben dann den Suffix ‘Sync’ im Namen (rename versus renameSync). Bei den synchronen Methoden entfällt die Rückruffunktion.

fs.rename(oldPath, newPath, callback) benennt eine Datei um. Mit fs.ftruncate(fd, len, callback) leeren Sie eine Datei. Dabei wird entweder ein Dateibeschreibungsobjekt benutzt oder der Pfad zur Datei.

Die Funktionsgruppe fs.fchown(fd, uid, gid, callback), fs.chown(path, uid, gid, callback) und fs.lchown(path, uid, gid, callback) setzt den Eigentümer einer Datei. Dabei wird entweder ein Dateibeschreibungsobjekt benutzt oder der Pfad zur Datei. Die Gruppe fs.fchown(fd, mode, callback), fs.chown(path, mode, callback) und fs.lchown(path, mode, callback) setzt Rechte auf eine Datei. Dabei wird entweder ein Dateibeschreibungsobjekt benutzt oder der Pfad zu Datei.

Mit fs.fstat(fd, callback), fs.stat(path, callback) oder fs.lstat(path, callback) ermitteln Sie Informationen über eine Datei. Die Rückruffunktion hat zwei Argumente, err und stats. stats ist vom Typ fs.Stats. lstat verarbeitet bei symbolischen Links den Link selbst, nicht das Ziel des Links.

Mit fs.realpath(path[, cache], callback) ermitteln Sie den echten Pfad einer Datei.

1 var cache = {'/etc':'/private/etc'};
2 fs.realpath('/etc/passwd', cache, function (err, resolvedPath) {
3   if (err) throw err;
4   console.log(resolvedPath);
5 });

Die Methode fs.unlink(path, callback) löscht eine Datei. Mit fs.rmdir(path, callback) wird ein Ordner entfernt. Die Rückruffunktion hat keine zusätzlichen Argumente.

Mit fs.mkdir(path[, mode], callback) wird ein Ordner erzeugt. Der Zugriff auf den Ordner wird mit 0777 (alle haben alle Rechte) festgelegt.

fs.readdir(path, callback) dient dazu, einen Ordner zu lesen und alle Dateien darin als Dateiinformation in ein Array zu legen. Die speziellen Ordner ‘.’ und ‘..’ werden nicht mit aufgenommen.

Die Methode fs.close(fd, callback) schließt eine geöffnete Datei. Die Rückruffunktion hat keine zusätzlichen Argumente. fs.open(path, flags[, mode], callback) öffnet eine Datei dagegen zum Zugriff. Das Argument flags hat folgende Bedeutung:

mode setzt die Zugriffsrechte, wenn die Datei erzeugt wird. Der Standardwert ist 0666, schreiben und lesen.

Der Zeitstempel einer Datei kann mit fs.utimes(path, atime, mtime, callback) bzw. mit fs.futimes(fd, atime, mtime, callback) geändert werden.

Das Schreiben von Daten erfolgt mit fs.write(fd, buffer, offset, length[, position], callback). buffer liefert die Bytes, position die Position, aber der geschrieben wird, offset dagegen die Position im Puffer. Die Rückruffunktion gibt die geschriebenen Bytes an, einmal die Anzahl und den Puffer selbst. Alternativ kann fs.write(fd, data[, position[, encoding]], callback) benutzt werden.

Das Lesen von Daten erfolgt mit einem Beschreibungsobjekt fd mittels fs.read(fd, buffer, offset, length, position, callback):

Die Rückruffunktion gibt die Anzahl der wirklich gelesenen Bytes und den Puffer an.

Direkt auf einer Datei wird mit fs.readFile(filename[, options], callback) gearbeitet.

1 fs.readFile('/etc/passwd', function (err, data) {
2   if (err) throw err;
3   console.log(data);
4 });

Das Schreiben auf eine Datei erfolgt mit fs.writeFile(filename, data[, options], callback).

1 fs.writeFile('message.txt', 'Hello Node', function (err) {
2   if (err) throw err;
3   console.log('It\'s saved!');
4 });

Mittels fs.appendFile(filename, data[, options], callback) wird direkt an eine existierende Datei angehängt.

1 fs.appendFile('message.txt', 'data to append', 
2               function (err) {
3   if (err) throw err;
4     console.log('The "data to append" was appended to file!');
5   });

Die Methode fs.watch(filename[, options][, listener]) dient dazu, eine Datei auf Umbenennungen hin zu überwachen. Die Methode gibt eine Instanz vom Typ fs.FSWatcher zurück.

1 fs.watch('somedir', function (event, filename) {
2   console.log('event is: ' + event);
3   if (filename) {
4     console.log('filename provided: ' + filename);
5   } else {
6     console.log('filename not provided');
7   }
8 });

Es gibt eine Methode fs.exists(path, callback) die testet, ob eine Datei existiert. Die Benutzung ist jedoch nicht empfehlenswert.

Mit fs.access(path[, mode], callback) testen Sie die Zugriffsrechte eines des aktuellen Benutzers. Die Rückgabe enthält Werte aus einer Liste von Konstanten:

1 fs.access('/etc/passwd', fs.R_OK | fs.W_OK, function(err) {
2   util.debug(err ? 'no access!' : 'can read/write');
3 });
Funktionen zum Umgang mit Streams

Stream verarbeiten Daten byteweise, was meist effizienter ist.

Der Aufruf fs.createReadStream(path[, options]) gibt ein ReadStream-Objekt zurück. Das Argument options hat diese Standardwerte:

1 { 
2   flags: 'r',
3   encoding: null,
4   fd: null,
5   mode: 0666,
6   autoClose: true
7 }
1 fs.createReadStream('sample.txt', {start: 90, end: 99});

Mit fs.createWriteStream(path[, options]) wird ein Stream zum Schreiben erstellt, das Objekt ist vom Typ WriteStream.

1 { 
2   flags: 'w',
3   encoding: null,
4   fd: null,
5   mode: 0666
6 }

Express

Express ist die Middleware-Komponente einer Node-Applikation. Damit ist die Vermittlungsschicht zwischen dem Client und dem Backend mit seinen Persistenzfunktionen gemeint. Kernaufgabe ist das Routing.

8.4 Installation

Voraussetzung für Express ist eine funktionierende Node-Umgebung. Liegt diese vor, können Sie eine erste Applikation erstellen. Der hier gezeigte Ablauf stellt Express bereit, die eigentliche Infrastruktur müssen Sie aber manuell erstellen. Im Abschnitt Applikationsstruktur finden Sie Informationen, wie der Express-Generator eingesetzt werden kann, um dies zu vereinfachen.

Zuerst wird ein Ordner für die Applikation geschaffen:

1 mkdir SimpleApp
2 cd SimpleApp

Mit npm init wird dann eine Datei package.json erzeugt. Damit werden die Applikation und ihre Abhängigkeiten beschrieben.

1 npm init

Die Angaben für die Beschreibungsdatei werden im Dialog abgefragt. In den meisten Fällen ist es in Ordnung die Standards zu übernehmen. Drücken Sie also einfach mehrfach ENTER, außer für die Option entry point. Hier geben Sie folgendes ein:

1 entry point: app.js

Dies bestimmt, dass die Startdatei, also der Beginn der Applikation, app.js ist. Sie können natürlich jeden Namen wählen.

Nun wird Express installiert und in die Liste der Abhängigkeiten (Option --save) aufgenommen. Fügen Sie gegebenenfalls noch die Option -g hinzu, um Express global verfügbar zu machen. Das ist sinnvoll, wenn Sie planen, weitere Projekte mit Node zu entwickeln.

1 $ npm install express --save
Abbildung: Interaktive Installation (Ubuntu)
Abbildung: Interaktive Installation (Ubuntu)

8.5 Applikationsstruktur

Express liefert eine fertige Applikationsstruktur. Mit der Installation steht nicht nur das Express-Modul bereit, sondern die fertige Ordnerstruktur kann mit nur einem Befehl erstellt werden. Sie müssen dies jedoch nicht nutzen. Es ist durchaus möglich, eine Applikation ganz einfach auf einer einzigen Datei aufbauend zu erstellen.

Bei der Installation im vorherigen Abschnitt wurde bei der Initialisierung gesagt, dass die Startdatei app.js lautet. Diese könnte nun folgendermaßen aussehen:

 1 var express = require('express');
 2 var app = express();
 3 
 4 app.get('/', function (req, res) {
 5   res.send('Hallo Express!');
 6 });
 7 
 8 var server = app.listen(3000, function () {
 9   var host = server.address().address;
10   var port = server.address().port;
11 
12   console.log('Ich höre  auf http://%s:%s', host, port);
13 });

Hier wird zuerst Express selbst eingebunden und mit dem Konstruktor-Aufruf eine Applikation app erstellt. Dann wird eine Route festgelegt, sie Stammroute “/”. Alle anderen Aufrufe führen zu einem HTTP-Fehler 404 (Nicht gefunden). Dann wird der Endpunkt bestimmt, hier der Port 3000 auf dem lokalen System (Zeile 8). Trifft nun ein HTTP-Request ein, wird die Funktion der passenden Route ausgeführt. Im Beispiel wird dann der Text “Hallo Express!” ausgegeben. HTML gibt dieses Skript noch nicht zurück, dies muss alles separat erledigt werden. Es handelt sich jedoch bereits um eine korrekte HTTP-Kommunikation

Der Express-Generator

Zum Erzeugen einer Applikation kann der Express-Generator eingesetzt werden. Dieser steht als weiteres NPM-Paket zur Verfügung.

1 $ npm install express-generator -g

Der Generator verfügt über einige Optionen, erzeugt aber auch ohne weitere Angaben eine sinnvolle Umgebung.

Tabelle: Optionen des Express-Generators
Option Bedeutung
-v, –version Version
-e, –ejs EJS-Engine (siehe www.embeddedjs.com)
-hbs Handlebars-Engine
-H, –hogan Hogan-Engine (www.hogan.js)
-c, –css [CS] CSS-Precompiler
-f, –force Erzwinge Dateien in nicht-leeren Ordnern

Die Standard-Template-Engine ist Jade.

Der CSS-Precompiler kann einer der folgenden sein (Name und in Klammern die zu benutzende Option (option)):

Ohne Angabe wird einfaches CSS erwartet.

LESS oder SASS

In diesem Werk wird mit LESS (http://lesscss.org) gearbeitet. Prinzipiell ist das egal, wenn Sie bereits einen Favoriten haben, nutzen Sie diesen. Wenn beides neu ist, dann werden Sie mit LESS möglicherweise etwas glücklicher am Anfang, da es einfacher und weiter verbreitet ist (das heißt mehr Quellen zum Lernen und weniger Aufwand). Profis greifen dagegen oft zu SASS (http://sass-lang.com).

Der Generator erzeugt auch das Stammverzeichnis der Applikation, sodass Sie am besten im übergeordneten Verzeichnis beginnen:

1 express PortalApp
2 cd PortalApp
3 npm install

Mit dieser Befehlsfolge wird eine Applikation mit dem Namen PortalApp im Ordner PortalApp erstellt. Nun wird die Applikation im Debug-Modus gestartet:

set DEBUG=PortalApp & npm start

Die Standardadresse ist http://localhost:3000. Der Webserver basiert auf Node und weitere Einstellungen am Betriebssystem sind nicht erforderlich. Sie müssen hier weder IIS noch Apache oder sonst einen Server bereitstellen – es funktioniert, einfach so.

Folgende Struktur entsteht standardmäßig:

Abbildung: Struktur, die der Generator anlegt
Abbildung: Struktur, die der Generator anlegt

8.6 Routing in Node-Applikationen

Das Routing stellt einen Zusammenhang zwischen einem URL und einer ausführenden Instanz (Methode oder Modul) her. Immer wenn eine Applikationen mehr als eine Seite ausliefert, kommt Routing ins Spiel. Das trifft auch auf Single-Page-Applikationen (SPA) zu. Denn beim Stand heutiger Browser sind Sie gut beraten, wenn nicht alles in eine einzige Seite gepresst wird. Das grobe Raster der Applikation ist besser in mehreren serverseitigen Modulen aufgehoben, die ihrerseits jeweils gut als SPA ausgeführt werden können.

Das Routing kommt natürlich zu ganz anderer Bedeutung, wenn keine SPA erstellt wird. Dann geht es praktisch darum, die Auslieferung jeder einzelnen Seite zu steuern. Neben den Seiten ansich kümmert sich das Routing dann auch um die Parameter, die als Teil des URL mitgeliefert werden und den ausführenden Methoden zugeführt werden müssen.

Routing in Express

Wenn mehr Seiten einer Applikation hinzugefügt werden, werden mehr Routen benötigt. Dazu dient der Express Router. Dieser wird hier noch umfassend behandelt. Routen liefern aber nicht nur fertige Seiten aus. Wird ein Teil der Applikation im Single Page-Stil (SPA) entwickelt, nutzen Sie Express, um die Routen für ihre clientseitig programmierten Abrufe zu erstellen. Dies kann beispielsweise mit AngularJS erfolgen. Express bildet dann ein RESTful-Backend für AngularJS ab.

Erst die Kombination von serverseitiger Technologie und clientseitigen Elementen macht eine moderne Webapplikation aus.

Der Express Router

Der Express Router ist ein reines Routingmodul ohne viele Extras. Es gibt keine explizite Unterstützung von Views oder vordefinierte Einstellungen. Es gibt aber rudimentäre APIs wie use(), get(), param() und route(). Es gibt verschiedene Möglichkeiten, den Router zu benutzen. Die Nutzung von get() ist dabei nur eine Variante. Die folgende Beispielapplikation nutzt diese und einige andere Techniken. Am Ende des Texts finden Sie eine vollständige Beschreibung der gesamten API.

8.7 Eine Beispielapplikation

Die Beispielapplikation verfügt über einige Techniken, die in der Praxis sinnvoll eingesetzt werden können:

Nun wurde bereits mehrfach der Begriff Middleware benutzt. Doch um was handelt es sich dabei eigentlich im Zusammenhang mit Express?

Middleware – die Vermittlerschicht

Der Name Middleware ist trefflich gewählt. Die hier platzierten Funktionen werden nach dem Eintreffen der Anfrage vom Client und vor der Weiterleitung zur Beantwortung ausgeführt. Sie haben also maßgeblichen Einfluss auf die Verarbeitung der Anfrage. Eine Anwendung ist die Protokollierung von Anfragen. Diese finden in der Middleware statt, ohne Rücksicht auf die Arbeitsweise der anderen Komponenten – transparent und im Hintergrund.

Grundlegende Routen

Die Route zur Homepage wurde bereits definiert. Diese wie alle anderen Routen werden in der Datei app.js definiert. Diese Datei ist im Projekt der beste Platz, solange die Anzahl der Routen überschaubar ist. Da bei einer Single-Page-Applikation (SPA) nur das grobe Raster der Routen auf dem Server ausgeführt wird, ist dies in Ordnung. AngularJS kümmert sich dann um die Routen auf der Clientseite und regelt die Abfrage spezifischer Teil-Sichten mittels Parametern.

Definierte Routen reagieren auf spezifische Pfade und HTTP-Verben wie GET, POST, PUT/PATCH oder DELETE. Das funktioniert – mit oder ohne RESTful-Aktionen – solange nur eine Handvoll Routen benötigt werden.

Nun kann es vorkommen, dass doch komplexere Routen erforderlich werden. Komplexe Websites haben nicht nur einen Bereich, sondern auch Backend-Funktionen, Administrationsbereiche, Im- und Export, Reporting und vieles mehr. Jeder Bereich kann über unzählige Routen verfügen.

Die Funktion express.Router() realisiert eine Art Mini-Applikation. Sie erzeugen damit eine Instanz des Routers und definieren für diese Instanz einige Routen.

Listing: app.js
 1 // Die Applikationsinstanz wird gebildet
 2 var express = require('express');
 3 var app = express();
 4 
 5 // Eine neue Instanz des Routers wird erstellt
 6 var adminRouter = express.Router();
 7 
 8 // Die Admin-Site (http://localhost:3000/admin)
 9 adminRouter.get('/', function(req, res) {
10   res.send('Startseite des Admin-Bereichs!');
11 });
12 
13 // Die Benutzer-Site (http://localhost:3000/admin/users)
14 adminRouter.get('/users', function(req, res) {
15   res.send('Alle Benutzer anzeigen!');
16 });
17 
18 // Die Artikel-Seite (http://localhost:3000/admin/article)
19 adminRouter.get('/article', function(req, res) {
20   res.send('Alle Artikel anzeigen!');
21 });
22 
23 // Zuweisen der Routen an die Applikation
24 app.use('/admin', adminRouter);
25 
26 // Der Server
27 var server = app.listen(3000, function() {
28   console.log('Server gestartet');
29 });

Die Routen werden quasi isoliert erstellt und dann als Gruppe der Applikation zugewiesen. Die Pfade werden dabei addiert. Der Stammpfad wird durch die Methode use bestimmt. Die Anweisung könnte auch folgendermaßen aussehen:

app.use('/app', router)

Solche Miniapplikationen lassen sich mehrfach zuweisen und damit allein gewinnen Sie einiges an Übersichtlichkeit. Logisch getrennte Bereiche, wie beispielsweise Views und REST-API lassen sich nun auch im Quellcode sauber auseinanderhalten.

Die Router-Middleware (router.use())

Generell greift die Middleware vor der eigentlichen Verarbeitung ein. Dies ist sinnvoll für eine Reihe von Aufgaben:

Die Definition der Funktionen erfolgt in eben der Reihenfolge wie sie später benutzt werden. Die Einrichtung erfolgt nach der Erstellung der Applikation und vor dem Zuweisen der Routen. Dass folgende Beispiel zeigt, wie sämtliche Anfragen auf der Konsole ausgegeben werden.

1 // Funktion, die auf jede Anfrage reagiert
2 adminRouter.use(function(req, res, next) {
3   // Konsolenausgabe
4   console.log(req.method, req.url);
5   // Weiter mit der regulären Verarbeitung
6   next();
7 });

Entscheidend ist der Aufruf der Methode next(). Damit wird Express mitgeteilt, dass die Methode abgearbeitet wurde und mit der regulären Verarbeitung fortgesetzt werden kann. adminRouter.use() definiert die Middleware-Funktion. Die eigentliche Funktionalität ist selbst zu implementieren und damit reines JavaScript.

Die Reihenfolge der Anmeldung der Funktionen bestimmt auch die Reihenfolge der Verarbeitung. Nach der Route ist kein Platz für die Middleware-Funktionen, weil die Verarbeitung der Anfrage dort mit dem Aussenden der Daten endet.

Routen strukturieren

Bisher wurde bereits gezeigt, wie Routen abschnittsweise vergeben werden können. Die Vorgehensweise ist bei den meisten Projekten ähnlich. Die Startseite mit ihren wichtigsten Links ist ein Bereich, die Administration ein anderer. Eine API – RESTful oder nicht – sollte immer separat geführt werden. Damit haben Sie genug Spielraum, um bei Erweiterungen nicht die Übersicht zu verlieren. Die Festlegung der Bereiche sieht dann so aus:

1 app.use('/', basicRoutes);
2 app.use('/admin', adminRoutes);
3 app.use('/api', apiRoutes);

Routen mit Parametern (/hello/:id)

Der Aufruf einer Seite alleine reicht meist nicht aus. Werden Daten aus Datenbanken abgerufen, müssen Parameter übertragen werden. Der Aufbau der URL ist nahezu beliebig. Sie müssen allerdings die Grenzen von HTTP beachten. Die Länge einer URL ist auf 2000 Zeichen begrenzt. Außerdem lädt eine komplexe URL zum Spielen ein, die Angaben sind bei den meisten Browsern klar sichtbar und leicht zu manipulieren. Je komplexer die URL desto höher ist der Aufwand für die Validierung der Parameter.

Wenn Sie Daten aus Datenbanken abrufen, bietet es sich an alle Abrufe auf den Primärschlüssel zu beschränken. Das führt in der Geschäftslogik möglicherweise dazu, das Daten aus verbundenen Tabellen oder Dokumenten erneut geladen werden. Im Ausliefern solcher Anfragen sind Datenbanken aber richtig gut und die Vereinfachung bei der Gestaltung des Servercodes ist fast immer wertvoller. Nennen Sie ihren primären Parameter dann konsequenterweise immer id.

In der Beschreibung der Route werden Parameter mit einem Doppelpunkt eingeleitet:

1 adminRouter.get('/users/:id', function(req, res) {
2   res.send('Benutzer-ID: ' + req.params.id + '!');
3 });

Der Router erkennt dies und überträgt die Werte in ein Objekt mit dem Namen params, das Teil des Anforderungsobjekts req ist. Dort stehen die Parameter als Eigenschaften zur Verfügung. Die URL für dieses Beispiel sieht folgendermaßen aus:

http://localhost:3000/admin/users/123

Der Pfad-Abschnitt admin wurde im Router selbst definiert, der spezifische Pfad legt user fest und 123 wird an die Eigenschaft id übergeben. Der Doppelpunkt dient der Erkennung und ist nicht Teil des Pfades.

Durch die hohe Anfälligkeit für Manipulationen müssen Parameter immer validiert werden. An dieser Stelle kommt wieder die Middleware-Schicht ins Spiel. Sie stellt eine Methode param() zur Verfügung, der die Parameter übergeben werden, bevor sie der Verarbeitung zugeführt werden.

Router-Middleware für Parameter (.param)

Die Funktion param() bildet die Parameter für eine bestimmte Route ab. Auch dieser Aufruf muss vor der Abarbeitung der Anforderung stehen. Innerhalb der Funktion können dann die Parameter untersucht, modifiziert oder sonstwie behandelt werden.

Das folgende Beispiel zeigt, wie der Parameter id geprüft wird:

Listing: param_sample.js
 1 adminRouter.param('id', function(req, res, next, name) {
 2   console.log('Validierung für ID ' + id);
 3   var id = Number(req.params.id);
 4   if (!id){
 5     // Fehlerbehandlung
 6   } else {
 7     // Ablage des geprüften Wertes
 8     req.id = id;
 9     // Weiter mit Verarbeitung
10     next();
11   }
12 });
13 
14 adminRouter.get('/users/:id', function(req, res) {
15   res.send('ID: ' + req.id + '!');
16 });

Eine gültige URL ist hier:

http://localhost:3000/admin/users/123

Wie die Fehlerbehandlung hier aussieht, hängt wieder von der Aufgabenstellung ab. Eine Website für (menschliche) Benutzer verlangt sicher andere Reaktionen als eine RESTful-API, die möglichweise auf technische Fehler reagieren muss.

Mehrere Routen (app.route())

Die Funktion app.route() ist ein direkter Aufruf des Routers und entspricht dem Aufruf express.Router(). Die Funktion verfügt aber außerdem über die Möglichkeit mehrere Routen in einem Schritt zu erstellen und mehrere Aktionen über eine Route abzubilden. Das letzte vermeidet, dass bei hunderten Aktionen ebenso viele Routen erstellt werden müssen.

Im folgenden Beispiel wird eine Route /login definiert. Auf diese reagieren zwei Methoden. Einmal wird das Verb GET ausgewertet, einmal POST.

Listing: login_sample.js
1 app.route('/login') 
2    .get(function(req, res) {
3       res.send('Das Anmeldeformular.');
4    })
5    .post(function(req, res) {
6       console.log('Anmelden');
7       res.send('Anmeldung verarbeitet!');
8    });

app ist im Beispiel das zentrale Applikationsobjekt und die Definition erfolgt überlicherweise in der Datei app.js.

Die Vorgehensweise ist typisch für alle Arten von Formularen. Wird die Seite im Browser mit http://localhost:3000/login aufgerufen, erzeugt der Browser eine GET-Anfrage. Der Benutzer sieht das Formular und füllt es aus. Er sendet es dann mit der Sende-Schaltfläche (submit) ab. Der Browser erstellt nun eine POST-Anfrage und fügt die Formulardaten an.

Man spricht nun von Aktionen einer Route. Im letzten Beispiel waren es zwei Aktionen. Eine RESTful-API könnte noch auf weitere Verben mit derselben Route reagieren.

8.8 Pug

Pug ist eine Template-Engine für Express, der Middlware- und Routing-Lösung für Node.js. Sie ist der Standard für Express. Wenn Sie sich also intensiv mit Node.js und Express auseinandersetzen, führt kein Weg an Pug vorbei.

Pug nutzt eine vereinfachte Darstellung der HTML-Seite durch simple Textbefehle. Praktischerweise entsprechen diese den Namen der HTML-Tags. Da HTML eine Hierarchie aufbaut und Pug keine schließenden Tags kennt, muss die Baumstruktur anders entstehen. Pug nutzt dazu Einrückungen im Texteditor. 2 Leerzeichen zeigen an, dass das folgende Element ein Kindelement ist.

Vorbereitung

Pug setzt voraus, dass Sie mit node.js arbeiten und die Middleware Express nutzen. Der einfachste Weg zu einer funktionierenden Umgebung geht über ein schrittweises Abarbeiten der Bausteine einer node.js-Installation.

Damit steht die Umgebung, und der Beschäftigung mit Pug steht nichts im Weg.

Installationsanleitung

Zur Installation gibt es wenig Besonderheiten. Pug benötigt Express. Falls dies mit der Videoanleitung bereits installiert wurde, kann dieser Schritt überprungen werden. Ansonsten folgen Sie der Schrittfolge hier.

Installieren Sie nun das Pug-Paket. Ich habe meine Projekte unter /home/joerg/Apps und für dieses Bändchen dann unter Pug abgelegt. Legen Sie dann einen Ordner views an, in dem eine erste Testseite entstehen wird (sie befinden sich in Apps):

1 mkdir Pug
2 cd Pug
3 npm install express
4 npm install Pug
5 npm init
6 npm install express --save
7 mkdir views
Abbildung: Beschreibung der Applikation
Abbildung: Beschreibung der Applikation

npm init erstellt die Node.js-Applikation. Dabei wird eine Paket-Datei erzeugt, deren Werte interaktiv abgefragt werden. Die erzeugte Paket-Datei kann noch manuell angepasst werden.

Legen Sie in dem neu erstellten Applikationsverzeichnis Pug eine Datei mit dem Namen index.js an. Sie hat folgenden Inhalt:

1 var express = require('express');
2 var app = express();
3 
4 app.get('/', function (req, res) {
5   res.send('Hallo Express!');
6 });
7 
8 var server = app.listen(3000, function () {});

Starten Sie nun den Node-Server:

1 npm start
Abbildung: Start der Applikation
Abbildung: Start der Applikation

Geben Sie nun im Browser auf dem Entwicklungssystem folgende URL ein: http://localhost:3000. Sie sollten dann die “Hallo Express!”-Ausgabe sehen.

Abbildung: Ausgabe der Seite
Abbildung: Ausgabe der Seite

Applikationsstruktur

Express bietet eine Reihe spannender Funktionen. Ich will hier jedoch nur auf Pug eingehen und deshalb ist das manuelle Erzeugen und nutzen einer View einfacher. Lesen Sie “Jörgs Webbändchen” zu Express, um mehr über die Applikationsstruktur herauszufinden.

Die einfachste Nutzung von Pug besteht aus zwei Bausteinen. Zum einen die erste View, index.pug:

Datei: index.pug
1 doctype html
2 html(lang='en')  
3   head  
4     title= title  
5   body!= body  
6     h1= title

Zum anderen wird das “Hallo Express”-Beispiel so verändert, dass nun statt des statischen Texts die View benutzt wird:

Datei: index.js
 1 var express = require('express');
 2 var app = express();
 3 app.set('view engine', 'pug');
 4 
 5 app.get('/', function (req, res) {
 6   res.render('index', { 
 7     title: 'Hallo pug!' 
 8   });
 9 });
10 
11 var server = app.listen(3000, function () {});

Zum einen wird hier Pug als Standard vereinbart, sodass keine Dateierweiterung angegeben werden muss und das passende Modul vorab geladen werden kann. Dies passiert durch:

app.set('view engine', 'pug');

Dann wird statt res.send die Funktion res.render benutzt. Der erste Parameter ist der Name der View, der ohne Pfad (standardmäßig wird im view-Ordner gesucht) und ohne Dateierweiterung (standardmäßig wird nun pug benutzt) angegeben werden kann. Der zweite Parameter ist ein Objekt, dass lokale Variablen für die View bestimmt. Jede Eigenschaft des Objekts wird als lokale Variable bereitgestellt. Im Beispiel ist das der Wert title.

Pug-Views

Statt HTML schreiben Sie ab jetzt die Ansichtsseiten in pug. Noch einmal das eben benutzte Beispiel:

Datei: index.pug
1 doctype html
2 html(lang='en')  
3   head  
4     title= title  
5   body!= body  
6     h1= title

Auf jeder Zeile der View steht zuerst ein HTML-Tag. Statt der Schreibweise in XML-Form (<title></title>) nimmt Pug hier eine vereinfachte Darstellung.

title= title

Der linke Teil ist das HTML-Element. Es folgt ein Gleichheitszeichen, dass die Kodierung bestimmt, also die Behandlung von HTML-spezifischen Entitäten wie < oder >. Dann folgt JavaScript. Da eine lokale Variable mit dem Namen title vereinbart wurde, wird dieser Ausdruck hier hingeschrieben. Analog funktioniert das mit h1, dass unterhalb des body-Elements steht. Der Umgang mit body zielt darauf ab, dass Views üblicherweise auf Stammseiten (Layout- oder Master-Seiten) basieren und der eigentliche Inhalt über die Variable body (rechts im Ausdruck) zugeordnet wird. Da HTML aus einer Seite direkt übernommen werden soll, wird der Operator != benutzt, der nicht codiert.

Abbildung:Ausgabe durch die View
Abbildung:Ausgabe durch die View
Umgang mit Teil-Ansichten

Teil-Ansichten (partial views) erlauben das Strukturieren von Views. Eine Pug-View sieht beispielsweise folgendermaßen aus:

Datei: index.pug
1 doctype html
2 html(lang='en')  
3   head  
4     title= title  
5   body!= body  
6     include navigation
7     h1= title

Mit dem Befehl include wird eine weitere View eingebunden, navigation.pug. Beachten Sie, dass diese ohne Anführungszeichen und Klammern angegeben wird.

Diese Navigation wird nun in einer weiteren Datei erstellt: views/navigation.pug:

Datei: navigation.pug
1 div#navigation
2   a(href='/') home
Abbildung
Abbildung
Umgang mit Layout-Seiten

Eine Layout-Seite ist ein Master, eine Stammseite deren Inhalte von Inhaltsseiten bestimmt werden. Das entspricht der Layout-Seite in ASP.NET MVC oder der Master-Seite in ASP.NET.

Eine Layout-Seite sieht beispielsweise folgendermaßen aus:

Datei: index.pug
1 doctype html
2 html(lang='en')  
3   head  
4     title= title  
5   body!= body  
6     include navigation

Dies unterscheidet sich in kaum von dem vorherigen Beispiel. Lediglich das h1-Element am Ende fehlt.

Im nächsten Schritt wird die Inhaltsseite erstellt. Sie heißt views/content.pug:

Datei: content.pug
1 extends index
2 
3 h1= title
4   a(href='http://www.joergkrause.de/') Jörg &lt;Is A Geek&gt; Krause

Sie verweist auf die Layout-Seite. Nun wird das Startskript angepasst, denn Pug rendert zuerst die Inhaltsseite, die ihrerseits die Layout-Seite aufruft.

Datei: index.js
 1 var express = require('express');
 2 var app = express();
 3 app.set('view engine', 'pug');
 4 
 5 app.get('/', function (req, res) {
 6   res.render('content', { 
 7     title: 'Hallo Pug!' 
 8   });
 9 });
10 
11 var server = app.listen(3000, function () {});

Beachten Sie die res.render-Funktion, die nun content statt vorher index aufruft (Zeile 5).

Jetzt kann der node-Server gestartet werden (im Ordner wo die Datei package.json steht):

npm start

Soweit der Standardport nicht anderweitig vergeben wurde zeigt der Browser die gerenderte HTML-Seite nun an:

http://127.0.0.1:3000/

Der Einstiegspunkt ist der Aufruf von res.render mit dem Argument der Inhaltsseite, content.pug. Die Engine sorgt dann für das Laden der Layout-Seite und die Verarbeitung. Der gesamte Vorgang findet also auf dem Server statt.

Abbildung: Ausgabe mit Layout-Seite
Abbildung: Ausgabe mit Layout-Seite

Dabei fällt auf, dass die Navigation verschwunden ist. Das ist das normale Verhalten. Denn nun wurde der Inhalt des body-Elements tatsächlich durch eine Inhaltseite geliefert und damit wird der statische Inhalt überschrieben. Freilich gibt es hier einige Optionen, dieses Verhalten zu verändern. Dies wird in der Sprachreferenz genau beschrieben.

Sprachreferenz

Im Folgenden finden Sie eine systematische Sprachreferenz basierend auf der Original-Dokumentation. Der Einstieg in die online verfügbaren Informationen ist Github. Ergänzend zu diesem Buch findest du eine deutsche Srpachreferenz auf meiner Homepage unter folgender Adresse:

8.9 Die Client-Bibliotheken

Die Client-Seite ist bei modernen Web-Anwendungen mindestens ebenso wichtig wie der Server. Letztlich geht es nicht nur um eine hervorragende Benutzererfahrung, sondern auch um eine effektive Steuerung der erforderlichen Datentransfers zwischen Server und Client. Dies kommt erheblich der Performance des Gesamtsystems zugute.

Ebenso wichtig ist der Client heute aus Sicht der Skalierbarkeit. Wenn du eine Plattform planst, die Millionen von Nutzern adressiert, dann ist es eben ein wesentlicher Unterschied, ob ein Teil des Codes auf Millionen Rechnern der Benutzer ausgeführt wird oder millionenfach auf einem zentralen Server. Die extreme Distribution der Anwendung spart Bandbreite, Rechenleistung und nutzt letzlich allen. Du kannst heute gut davon ausgehen, dass die für Web-Anwendungen benutzten Geräte, also PC, Mac oder Smartphone hinreichend leistungsfähig sind.

Es gibt viele Frameworks, die clientseitig zum Einsatz kommen können. Hier sollen die wichtigstens vorgestellt werden, um die ersten Schritte in der Client-Welt gehen zu können.

jQuery

Die bekannteste Bibliothek ist jQuery. jQuery wurde bereits 2006 durch John Resig entworfen, weil er es leid war ständig um die Unterschiede der verschiedenen Implementierungen im Browser herum zu programmieren. jQuery abstrahiert das Objektmodell des Browsers (DOM – document object model) und nivelliert die spezifischen Eigenheiten weitgehend.

Entsprechend gibt es auch browserspezifische Versionen. Version 2 unterstützt auch alte Internet Explorer der Version 7 bis 9, während Version 3 dies nicht mehr tut. Der Vorteil der fehlenden Unterstützung liegt in der Verringerung der Größe der Bibliothek.

jQuery ist nur eine Sammlung wertvoller Funktionen, kein vollständiges Rahmenwerk zum Programmieren. Durch ein cleveres Plug-In-Konzept lassen sich weitere Funktionen leicht einbinden. Auf diese Weise entstand auch jQuery-UI – eine Bibliothek mit Anzeigeelementen. Auch das bereits vorgestellte Bootstrap basiert auf jQuery.

Einsatzmöglichkeiten

Eine der wichtigsten Funktionen ist, die Möglichkeit, Elemente auf der Website (DOM) auszuwählen, um dann später damit Aktionen auszuführen. Dazu gehört das Aussehen zu ändern, Animationen zu starten oder auf Benutzerereignisse zu reagieren.

Was in JavaScript umständlich und unsicher über getElementById und getElementsBy… gemacht wurd, geht mit jQuery problemlos über $("#id") und $(".classe"). Beim Auswählen der Elemente bist du sehr nahe an der Schreibweise von CSS. Da CSS ohnehin benötigt wird, hält sich der Lernaufwand in Grenzen.

Die Funktionen umfassen also:

jQuery einbinden

Es gibt 2 Arten um jQuery einzubinden. Du kannst alle benötigten Dateien von jQuery herunterladen und auf dem eigenen Webspace bereitstellen http://jquery.com oder einfach einen Link auf ein CDN ( Content Delivery Network) setzen und dann jQuery nutzen.

Lokal einzubinden ist lediglich eine Datei:

1 <script src="pfad-zur-jquery/jquery.js"></script>

Oder du kannst die aktuellste Version von jquery.com nutzen:

1 <script src="http://code.jquery.com/jquery-latest.js"></script>

jQuery kann auch über Googles CDN eingebunden werden.

1 <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jque\
2 ry.min.js"></script>
jQuery nutzen

Nach dem Einbinden der Bibliothek wird der passende Code geschrieben. Dabeio handelt sich normalerweise um JavaScript. Es ist möglich, jQuery mit TypeScript zu schreiben, setzt aber eine etwas andere Herangehensweise voraus. Dazu später mehr.

Statt den Code einfach in dies Seite zu packen, solltest du immer das folgende Standardvorgehen wählen:

1 <script>
2 $(document).ready(function(){
3     /* Hier der jQuery-Code */
4 });
5 </script>

Es gibt auch die Kurzschreibweise mit dem Dollarzeichen:

1 <script>
2 $(function(){
3     /* Hier der jQuery-Code */
4 });
5 </script>

Dies stellt sicher, dass der Code erst ausgeführt wurde, wenn der Browser auf dem Objekt document das Ereignis ready ausgeführt hat. Das ist der Fall, wenn alle Elemente des DOM geladen wurden und im Speicher des Computers zur Bearbeitung bereit stehen.

Das Zeichen $ ist eine Kurzschreibweise für jQuery. Es handelt sich technisch um einen normalen Namen. Das $-Zeichen hat in JavaScript keine spezielle Bedeutung und jQuery hat es sich quasi reserviert. Achte darauf, dass dies nicht zwingend der Fall sein muss – theoretisch könnten sich andere Bibliotheken diesen Namen auch reservieren.

Der komplette Quellcode in HTML5 sieht dann für die Client-Seite folgendermaßen aus:

 1 <!DOCTYPE html>
 2 <html lang="de">
 3 <head>
 4 <title>jQuery Beispiel-Code</title>
 5 <script src="http://code.jquery.com/jquery-latest.js"></script>
 6 <script>
 7 $(document).ready(function(){
 8     /* Hier der jQuery-Code */
 9     alert('Hallo Welt');
10 });
11 </script>
12 </head>
13 <body>
14 <h1>jQuery Beispiel-Code</h1>
15 </body>
16 </html>

Das ist jetzt noch nicht wirklich mehr als mit bisherigem JavaScript auch möglich ist. Aber das ist das Standardvorgehen, um jQuery einzubinden und Code einzugeben. Der JavaScript-Teil wird in der Praxis meist in eine weitere Datei ausgelagert. Dazu speicherst du folgenden Teil in die Datei scripts.js (am einfachsten in den selben Ordner in dem die HTML-Datei liegt).

1 $(document).ready(function(){
2     /* Hier der jQuery-Code */
3     alert('Hallo Welt');
4 });

Achte darauf, dass in einer externen Datei keine <script>-Tags benutzt werden – es ist bereits JavaScript und dies muss nicht noch angezeigt werden.

Im <head>-Bereich der HTML-Datei muss nun nur noch die Verknüpfung erfolgen:

 1 <!DOCTYPE html>
 2 <html lang="de">
 3 <head>
 4   <title>jQuery Beispiel-Code</title>
 5   <script src="http://code.jquery.com/jquery-latest.js"></script>
 6   <script src="scripts.js"></script>
 7 </head>
 8 <body>
 9 <h1>jQuery Beispiel-Code</h1>
10 </body>
11 </html>

Der Browser kan nun auch den eigenene Code im Cache ablegen und damit die Seite beim erneuten Aufruf schneller laden.

Auswählen von Elementen

Bevor du mit irgendwelchen Elementen etwas anstellen kannst, musst du das gewünschte Element erst auswählen. Die Auswahl machst du über den Typ-Selektor.

Listing: jQuery-Code (script.js)
1 $(function(){
2     $('#output).text("Hallo jQuery");
3 });
Listing: HTML-Code (nur body)
1 <body>
2 
3 <h1>jQuery Beispiel-Code</h1>
4 
5 <div id="output"></div>
6 
7 </body>

Hier wird ein Element der Seite, <div> durch das Attribut id adressierbar gemacht. Der Identifizierer lautet output, was vollkommen willkürlich ist. in jQuery wird dann der Selektor-Befehl $('') benutzt, um darauf zuzugreifen. Analog zur Schreibweise in CSS wird #output zur Adressierung benutzt. Hier eine kurze Zusammenfassung typischer Adressierungsverfahren:

Die üblichen Verknüpfungen in CSS mit >, ~ usw. funktionieren auch.

Ändern von Inhalten

Inhalte lassen sich auslesen und ändern. Das vorherige Beispiel hat das bereits benutzt, indem mit text der Inhalt des <div>-Tags verändert wurde.

1 var heading = $('h1').html();
2 var para = $('p').html();
3 $('h1').html(para);
4 $('p').html(heading);

Dieses Skript liest den Text aus einem Element mit dem Namen <h1> und schreibt sie in ein Element mit dem Namen <p>.

Nun können diese Elemente aber mehrfach vorkommen. Tatsächlich arbeitet jQuery intern immer mit Arrays, um eine Mehrfachauswahl nutzen zu können. Das ist nicht immer sinnvoll, weshalb eine möglichst genaue und sorgfältige Adressierung wichtig ist. Werden aber wirklich mehrere Elemente adressiert, ist dieses Verfahren äußerst leistungsfähig.

HTML-Klassen hinzufügen und entfernen

Viele Effekte und Aufgaben, die sich mit jQuery verwirklichen lassen, machen es nötig, eine CSS-Klasse zu einem Element hinzu zu fügen oder zu entfernen. Mit jQuery geht das einfach. Für das folgende Beispiel lege eine einfache CSS-Regel an, entweder im HTML-Head der Seite oder direkt im globalen CSS Ihres Projekts:

1 a.test { font-weight: bold; }

Diese Regel sorgt dafür, dass alle Hyperlinks Fett ausgezeichnet werden.

Füge nun folgenden Code in die Datei script.js ein:

1 $("a").addClass("test");

Alle Hyperlinks werden nun Fett dargestellt.

Um die Klasse mit jQuery wieder zu entfernen, verwende den folgenden Code:

1 $("a").removeClass("test");
Effekte

jQuery bietet eine Reihe von praktischen Effekten, die mit dem Aufruf einer Funktion genutzt werden können. Probieren beispielsweise für die Hyperlinks folgendes aus:

1 $("a").click(function(event){
2   event.preventDefault();
3   $(this).hide("slow");
4 });

Wenn der Benutzer nun auf einen Link klickt, wird dieser langsam verschwinden. Nicht besonders sinnvoll, aber durchaus effektvoll. jQuery ist übrigens durch diese Effekte bekannt geworden – 2006 konnte man damit noch beeindrucken.

8.10 Optimierung

Komplette Web-Anwendungen sind komplex und bestehen aus vielen Bausteinen. Der kritische Teil dabei ist der Ladevorgang – das Übertragen der ganzen Dateien, bestehend aus HTML, CSS, Fonts, Bildern und JavaScript.

Um dies zu verbessern, haben sich Techniken wie WebPack etabliert. Es gibt weitere System, aber WebPack ist führend und soll hier stellvertretend kurz vorgestellt werden.

Anwendungen mit WebPack verpacken

WebPack ist ein Programm zum Bündeln von Dateien. Jede Datei, die an einen Browser via HTTP übertragen wird, erfordert protokollspezifische Kopfzeilen. Das können einige Hundert Byte sein. Es erfordert auch ein Handshake – einen expliziten Kommunikationsvorgang. Beides kostet Bandbreite und Zeit. Weniger Dateien sind also ein Performancegewinn. Da sich Dateien aber oft in einem Abhängigkeitsverhältnis befinden, komplexe Ladestrategien herrschen oder inhaltsspezifische Besonderheiten zu beachten sind, ist das Verpacken eine echte Herausforderung. Zeit für ein weiteres Werkzeug.

WebPack kann Taskrunner wie Gulp durchaus ersetzen, wenn Gulp nur zum Verpacken benutzt wurde. Es geht aber auch gut in Kombination. Gulp alleine führt jedoch auch zu einer isolierten Betrachtung der Aufgaben – lesen, verdichten, kombinieren, verpacken, bereitstellen. Da fehlt manchmal der Blick fürs große Ganze. Vor allem hochoptimierte und produktionsreife Umgebungen sind mit Gulp nur sehr aufwändig erstellbar. WebPack kann erkennen, was in der Produktion wirklich benötigt wird und dann die Ladezeiten maximal reduzieren. Der Vorgang ist darüberhinaus auch weitgehend automatisch.

WebPack ist Teil des Entwicklungsprozesses und greift beim Verteilen – dem Deploymentprozess. Dies umfasst folgende Aufgaben:

Es gibt also gute Gründe, WebPack zu nutzen. WebPack kann rein serverseitig benutzt werden, bzw. zum Entwicklungszeitpunkt, um statische Versionen der zu ladenden Dateien zu erstellen. Es kann aber auch Dateien dynamisch nachladen und läuft dann – auch partiell – im Client.

Erste Schritte

Wie immer beginnt die Arbeit mit dem Laden der Bibliotheken:

1 npm i -g webpack webpack-dev-server@2

Hier wird WebPack global installiert, weil meist mehrere Projekte davon profitieren. Lies den Abschnitt zu npm, um mehr über die Installationsoptionen zu erfahren. Der zweite Teil, webpack-dev-server, ist ein Testserver, der das Starten der Anwendung lokal ermöglicht. Damit kannst du dieses Kapitel durcharbeiten, ohne den Text über Node und Express kennen zu müssen. Wenn du bereits eine laufende Node-Applikation erstellt hast, kannst du auch diese weiter nutzen.

Die Arbeit beginnt mit der Erstellung einer Konfigurationsdatei webpack.config.js im Wurzelverzeichnis der Applikation:

 1 const path = require('path');
 2 const webpack = require('webpack');
 3 module.exports = {
 4   context: path.resolve(__dirname, './src'),
 5   entry: {
 6     app: './app.js',
 7   },
 8   output: {
 9     path: path.resolve(__dirname, './dist'),
10     filename: '[name].bundle.js',
11   },
12 };

__dirname ist der Stammname des Ordners der Applikation. Diese Konstante wird von NodeJS bereitgestellt. Der Code in der Datei exportiert ein Konfigurationsobjekt. Dieses Objekt steuert die Verarbeitung:

Weil viele Dateien betroffen sein können, arbeitet WebPack mit Platzhaltern. Diese stehen zur Abwechslung mal im eckigen Klammern ([name]). Stelle dir vor, du, hast eine einzige Quelldatei mit dem Namen app.js im Ordner ./src. Diese hat in etwa folgenden Inhalt:

1 import moment from 'moment';
2 var rightNow = moment().format('MMMM Do YYYY, h:mm:ss a');
3 console.log(rightNow);

Nun startest du die Kommandozeile von WebPack – diese wird immer mit installiert – und dies sierht folgendermaßen aus:

1 webpack -p

Der Schalter -p steht for “production” und löst die Verkleinerung (Minimierung) der Dateien aus. Während der Entwicklung und bei der Benutzung des Debuggers im Browser wäre dies unter Unständen lästig.

Die Ausgabedatei trägt den Namen dist/app.bundle.js. Der spannende Teil ist allerdings der Umgang mit der Abhängigkeit. Im Skript wurde die Bibliothek moments benutzt (ein Node-Modul aus npm), mit der sich umfassende Datumsbechnungen anstellen lassen. WebPack erkennt dies und bindet die Datei und gegebenenfalls deren Abhängigkeiten mit ein.

Arbeiten mit mehreren Dateien

Die Anzahl der Eintritts- und Austrittspunkte ist nicht begrenzt. Die Eingabeform bestimmt das Verhalten. Um mehrere Dateien zu einer Ausgabedatei zusammenzufassen, wird folgende Konfiguration benutzt:

 1 const path = require('path');
 2 const webpack = require('webpack');
 3 module.exports = {
 4   context: path.resolve(__dirname, './src'),
 5   entry: {
 6     app: ['./home.js', './events.js', './vendor.js'],
 7   },
 8   output: {
 9     path: path.resolve(__dirname, './dist'),
10     filename: '[name].bundle.js',
11   },
12 };

Die Ausgabe erfolgt in dist/app.bundle.js in der Reihenfolge, die durch das Array in Zeile 6 bestimmt wird.

Sollen dagegen auch mehrere Ausgabedateien erzeugt werden, ändert sich die Konfiguration nur wenig:

 1 const path = require('path');
 2 const webpack = require('webpack');
 3 module.exports = {
 4   context: path.resolve(__dirname, './src'),
 5   entry: {
 6     home: './home.js',
 7     events: './events.js',
 8     contact: './contact.js',
 9   },
10   output: {
11     path: path.resolve(__dirname, './dist'),
12     filename: '[name].bundle.js',
13   },
14 };

Die Steuerung erfolgt auch hier über den Eingabepunkt. Statt eines Arrays mit mehreren Dateien werden nun mehrere Eintrittspunkte (Zeilen 6 bis 8) erzeugt. Die Namen der Eintrittspunkte, in dem JSON-Format die Eigenschaften, werden als Dateinamen für den Platzhalter benutzt. Die Ausgabedateien sind deshalb hier:

Bibliotheken einbinden

Große Anwendungen werden meist nicht als eine Datei ausgeliefert, sondern in wenigen Gruppen. Wenn die Anwendung Bootstrap und Angular nutzt, dann würde es praktisch auf drei Teile hinauslaufen: Bootstrap und Angular als Anbieter-Bibliotheken (vendor libraries) und dein Anwendung als ein dritter Teil.

Ein weiterer Anwendungsfall ergibt sich, wenn deine Anwendung nicht in allen Fällen alles benötigt. Warum sollte der Nutzer hunderte JavaScript-Dateien laden, wenn er nur wenige braucht? Auch wenn der Vorgan für den Nutzer unsichtbar abläuft, wirkt es sich auf die Ladezeit am Anfang aus.

Das Splitten in mehrere Teile übernimmt ein Plug-In, dass standardmäßig mitgeliefert wird. Dies trägt den Namen CommonsChunkPlugin. Es wird folgendermaßen benutzt:

 1 module.exports = {
 2   // …
 3   plugins: [
 4     new webpack.optimize.CommonsChunkPlugin({
 5       name: 'commons',
 6       filename: 'commons.js',
 7       minChunks: 2,
 8     }),
 9   ],
10 // …
11 };

Die Benutzung ist nicht ganz einfach. Die Option minChunks (Zeile 7) bestimmt, wie oft ein Modul geladen werden muss, damit es separat gebündelt wird. Das Bündel trägt den Namen commons.js im Beispiel. Wird das Modul aber nur einmal benötigt, bleibt es im Hauptmodul und wird nicht separiert. Die konkrete Ausgabe hängt also von der Benutzung im eigenen Code ab.

Zusätzliche Dateien erfordern zusätzliche Anforderungen an den Server. Das bedeutet auch zusätzliche Bandbreite für die Kopfzeilen. Aber die Cache-Funktion des Browsers muss hier mit berücksichtigt werden, die dafür sorgt, dass mehrfach benötigte Dateien nicht mehrfach geladen werden. Diese Balance zu finden ist Teil des Optimierungsprozesses einer Anwendung. Die Sorgfalt beim Umgang damit trägt dazu bei, ob eine Anwendung vom Benutzer bezüglich des Ladeverhaltens als gut oder sehr gut empfunden wird.

Manuelles Bündeln

Im vorherigen Abschnitt wurde bereits auf die Trennung von Anbieter-Bibliotheken verwiesen. Meist lohnt da eine manuelle Auftrennung ohne den Ladeautomatismus. Dies wird über einen speziellen Eintritspunkt mit dem Namen vendor gesteuert:

1 module.exports = {
2   entry: {
3     index: './index.js',
4     vendor: ['react', 'react-dom', 'rxjs'],
5   },
6   // …
7 }

Das CommonsChunkPlugin wird nicht benutzt, statt dessen werden die allgemeinen Bibliotheken react, react-dom und rxjs separat gebündelt – getrennt von der Anwendung.

Entwicklungsunterstützung

Zum Entwicklungszeitpunkt ist es manchmal gut, die Ausgabe schnell testen zu können. WebPack kommt deshalb mit einem eigenen Webserver. Vor allem beim Erstellen von Prototypen ist dies sinnvoll.

Zur Aktivierung wird der Abschnitt devServer in der Datei webpack.config.js benutzt:

 1 module.exports = {
 2   context: path.resolve(__dirname, './src'),
 3   entry: {
 4     app: './app.js',
 5   },
 6   output: {
 7     filename: '[name].bundle.js',
 8     path: path.resolve(__dirname, './dist/assets'),
 9     publicPath: '/assets',
10   },
11   devServer: {
12     contentBase: path.resolve(__dirname, './src'),
13   },
14 };

Die Ausgabe wird in die Startseite der Anwendung eingebunden:

Listing: src/index.html
1 <script src="/assets/app.bundle.js"></script>

Weitere statische Dateien, die die Anwendung benötigt, werden über publicPath (Zeile 9) im Ordner /assets erwartet.

Auf der Kommandozeile wird der Server nun folgendermaßen gestartet:

1 webpack-dev-server

Der Standardendpunkt des Servers ist localhost:8080. Der Pfad /assets im <script>Tag passt zu output.publicPath. Der Pfad ist also frei wählbar.

Beim entwickeln muss der Server nicht bei jeder Änderung neu starten. WebPack erkennt Änderungen und lädt die geänderten Teile sofort nach. Nur wenn diu Änderungen an der Konfiguration in webpack.config.js vornimmst, ist ein Neustart erforderlich.

Globale Methoden

Werden komplexere Funktionen benötigt, bläht das die Konfiguration auf. Deshalb können andernorts geschriebene Funktionen eingebunden werden:

1 module.exports = {
2   output: {
3     library: 'myClassName',
4   }
5 };

Das Bündel wird dann zu einer Instanz von window.myClassName hinzugefügt.

Ladefunktionen

Bislang ging es nur um JavaScript. Aber WebPack kann auch mit anderen Sprachen umgehen und übernimmt das Übersetzen von TypeScript oder die Transformation von SASS. Dazu werden Ladefunktionen benutzt, sogenannte Loader. Sie sind im Namen an dem Suffix -loader erkennbar, heißen also sass-loader oder babel-loader.

Loader am Beispiel Babel

Babel ist ein dynamisches Polyfill, eine Bibliothek, die fehlende Funktionen im Browser ergänzt. Babel kann beispielsweise benutzt werden, um ECMAScript 6 (die neue JavaScript-Version in 2016) zu nutzen, auch wenn einige Browser nicht alles verstehen. Babel ersetzt dann die fehlenden Implementierungen durch eigene, auf ES5 basierende Skripte.

Zuerst muss Babel selbst installiert werden:

1 npm i --save-dev babel-loader babel-core babel-preset-es2015

Und nun die Konfiguration:

 1 module.exports = {
 2   // …
 3   module: {
 4     rules: [
 5       {
 6         test: /\.js$/,
 7         exclude: [/node_modules/],
 8         use: [{
 9           loader: 'babel-loader',
10           options: { presets: ['es2015'] }
11         }],
12       },
13     
14       // Loaders for other file types can go here
15     ],
16   },
17   // …
18 };

Zuerst ist die Ladefunktionen selbst wichtig, hier also babel-loader. Die Optionen sind spezifisch für diesen Loader. WebPack wird angewiesen, den Loader nur mit bestimmten Dateien zu versorgen, hier mit dem Ausdruck /.js$/ (Zeile 6). Dies sind reguläre Ausdrücke. Lies das Kapitel zu regulären Ausdrücken, um hier die nötige Sicherheit zu gewinnen.

Kein Loader ist perfekt, und so kann es vorkommen, dass Dateien nicht mehr korrekt arbeiten, wenn sie geteilt oder kombiniert wurden. Deshalb kannst du einzelne Dateien von der Verarbeitung ausschließen. Im Beispiel wurde der gesamte Ordner node_modules mittels der Option exclude ausgeschlossen. Es handelt sich hier um ein Array, dessen Elemente reguläre Ausdrücke sind.

Loader für CSS

CSS kann analog zu JavaScript zusammengefasst und verdichtet werden. Damit WebPack das kann, wird css-loader und zum Verarbeiten der eigenen Stile style-loader benutzt:

1 npm add --save-dev css-loader style-loader
Listing: webpack.config.js
 1 module.exports = {
 2   // …
 3   module: {
 4     rules: [
 5       {
 6         test: /\.css$/,
 7         use: ['style-loader', 'css-loader'],
 8       },
 9       // …
10     ],
11   },
12 };

Die Verarbeitung beim mehreren Loadern findet in umgekehrter Reihenfolge bezogen auf das Array in Zeile 7 statt; hier wird also erst der css-loader arbeiten und dann erst der style-loader.

Der css-loader sorgt dafür, dass das CSS dynamisch über JavaScript geladen wird. Es wird also ein Request eingespart, weil das CSS quasi implizit im JavaScript steckt. Wenn die Anwendung das DOM dynamisch erstellt, wie es bei Angular der Fall ist, ist die Vorgehensweise sinnvoll. Außerdem erhälst du mehr Kontrolle, speziell über den FOUC-Effekt

FOUC

Als FOUC – Flash of unstyled content – wird ein Effekt verstanden, bei dem der Browser erst statische Inhalte anzeigt, dann CSS nachlädt, und dann erst die Stile anwendet. Das führt zu lästigem Aufblitzen unpassender Seitenteile oder anderer Effekte, die Benutzer irritieren.

Die Ladefunktion ist auch so intelligent, dass sie die @import-Anweisungen in den CSS-Dateien erkennt und auflöst. Das kann CSS zwar auch alleine, aber nur zum Preis weiterer Requests und browserabhängigem Laufzeitverhalten.

Das Laden von CSS via JavaScript ist eine herausragende Funktion, weil es mehr Steuermöglichkeiten gibt. Wenn eine Datei button.js existiert, die nur gelegentlich benötigt wird, und diese ein wenig CSS benötigt, dann wird das CSS nicht geladen, wenn der Button in der Datei nicht benötigt wird. Vor allem modulare CSS-Systeme, wie beispielsweise SMACSS profitieren davon. Auch Angular verpackt komponentenspezifisches CSS mit der Komponente und dies kann mit WebPack beim Ladeverhalten berücksichtigt werden.

Node-Module

We can use webpack to take advantage of importing Node modules using Node’s ~ prefix. If we ran yarn add normalize.css, we could use: @import “~normalize.css”;

… and take full advantage of NPM managing our third party styles for us—versioning and all—without any copy + pasting on our part. Further, getting webpack to bundle CSS for us has obvious advantages over using CSS’s default import, saving the client from gratuitous header requests and slow load times.

Update: this and the following section have been updated for accuracy, no longer confusing using CSS Modules to simply import Node modules. Thanks to Albert Fernández for the help!

CSS Modules

You may have heard of CSS Modules, which takes the C out of CSS. It typically works best only if you’re building the DOM with JavaScript, but in essence, it magically scopes your CSS classes to the JavaScript file that loaded it (learn more about it here). If you plan on using it, CSS Modules comes packaged with css-loader (yarn add –dev css-loader):

 1 module.exports = {
 2   // …
 3   module: {
 4     rules: [
 5       {
 6         test: /\.css$/,
 7         use: [
 8           'style-loader',
 9           {
10             loader: 'css-loader',
11             options: { modules: true }
12           },
13         ],
14       },
15       // …
16     ],
17   },
18 };

Note: for css-loader we’re now using the expanded object syntax to pass an option to it. You can use a string instead as shorthand to use the default options, as we’re still doing with style-loader.

It’s worth noting that you can actually drop the ~ when importing Node Modules with CSS Modules enabled (e.g.: @import “normalize.css”;). However, you may encounter build errors now when you @import your own CSS. If you’re getting “can’t find ___” errors, try adding a resolve object to webpack.config.js to give webpack a better understanding of your intended module order.

1 module.exports = {
2   //…
3   resolve: {
4     modules: [path.resolve(__dirname, './src'), 'node_modules']
5   },
6 };

We specified our source directory first, and then node_modules. So webpack will handle resolution a little better, first looking through our source directory and then the installed Node modules, in that order (replace “src” and “node_modules” with your source and Node module directories, respectively).

SASS

SASS ist ein Präprozessor für CSS.

Sass (Syntactically Awesome Stylesheets) ist eine Stylesheet-Sprache, die als Präprozessor die Erzeugung von Cascading Style Sheets erleichtert. Sie wurde ursprünglich beeinflusst von der Auszeichnungssprache YAML, von Hampton Catlin entworfen und von Natalie Weizenbaum entwickelt. (Quelle: Wikipedia)

Installiere zuerst SASS und die Ladefunktion

1 npm i --save-dev sass-loader node-sass

Füge dann der webpack.config.js folgendes hinzu:

 1 module.exports = {
 2   // …
 3   module: {
 4     rules: [
 5       {
 6         test: /\.(sass|scss)$/,
 7         use: [
 8           'style-loader',
 9           'css-loader',
10           'sass-loader',
11         ]
12       } 
13       // …
14     ],
15   },
16 };

Wenn nun im JavaScript eine Datei mit der Endung .scss oder .sass aufgerufen wird, greift der Loader und lädt diese, parst die Datei, konvertiert das SCSS in CSS und packt das fertige CSS in die Seite.

Abtrennen der CSS-Dateien

Es gibt viele Gründe, die Startdatei schlank zu halten und externe CSS-Dateien zu nutzen. Dazu wird der dynamische style-loader durch extract-text-webpack-plugin benutzt.

Die Startdatei der Applikation app.js enthält diesen Aufruf:

1 import styles from './assets/stylesheets/application.css';

Installiere den neuen Loader:

1 npm i --save-dev extract-text-webpack-plugin@2.0.0-beta.5

Füge dann der webpack.config.js folgendes hinzu:

 1 const ExtractTextPlugin = require('extract-text-webpack-plugin');
 2 module.exports = {
 3   // …
 4   module: {
 5     rules: [
 6       {
 7         test: /\.css$/,
 8         loader:  ExtractTextPlugin.extract({
 9           loader: 'css-loader?importLoaders=1',
10         }),
11       },
12     
13       // …
14     ]
15   },
16   plugins: [
17     new ExtractTextPlugin({
18       filename: '[name].bundle.css',
19       allChunks: true,
20     }),
21   ],
22 };

Nach dem Start von WebPack wird eine Datei app.bundle.css entstehen, die dann ganz klassisch via <link> in die HTML-Datei eingebunden werden kann.

HTML

So wie für CSS gibt es auch einen Loader für HTML: html-loader.

Viel wichtiger ist aber, dass HTML selten statisch erstellt wird. Oft ist es Teil einer Komponente (in React oder Angular), wird als JSX verpackt oder ist gleich ganz Teil eines Template-Systems wie Pug. Im HTML werden dynamische Elemente wie JSX oder Mustache oder Handlebars benutzt. Diesen Funktionen mit WebPack noch eine dynamische Ebene hinzuzufügen, ist selten sinnvoll und eher nicht anzuraten.

Denken in Modulen

WebPack erlaubt es, einen modularen Denkansatz zu pflegen. Du kannst deine Anwendung natürlich so schreiben:

1 └── js/
2     └── application.js   // 300KB Spaghetti-Code

Oder auch so:

 1 └── js/
 2     ├── components/
 3        ├── button.js
 4        ├── calendar.js
 5        ├── comment.js
 6        ├── modal.js
 7        ├── tab.js
 8        ├── timer.js
 9        ├── video.js
10        └── wysiwyg.js
11     
12     └── index.js  // ~ 1KB Code: 

Das ist schlank, gut lesbar und leicht wartbar. Das Laden ist eine Herausforderung, aber dafür gibt es ja WebPack, dass folgende Anweisung im JavaScript versteht:

1 imports from ./components/