7. Die Entwicklungsumgebung

Dieser Abschnitt beschreibt die Erstellung einer Entwicklungsumgebung. Moderne Softwareentwicklung ist heute oft damit verbunden, den fachlichen Anspruch durch geschicktes Einsetzen von vorhandenen Bausteinen zu erreichen. Das Mantra dazu lautet:

7.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 meist ohne weitere Kosten jederzeit verfügbar. Aufgrund dieser Rahmenbedingungen erhebt die Darstellung keinen Anspruch auf Vollständigkeit.

7.1.1 Paketverwaltung mit npm oder yarn

Pakete stammen aus npm – dem Node Package Manager. Als Client zum Zugriff dient entweder die Kommandozeile npm oder alternativ yarn.

npm ruft Pakete ab und installiert diese. Es verwaltet auch Versionen und Abhängigkeiten. Letzteres ist die größte Herausforderung.

7.1.2 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 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/.

7.1.2.1 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 (die globale Installation -g macht die Kommandozeile verfügbar):

1 $ npm install gulp -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.

7.1.2.2 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.

7.1.2.3 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’ liest 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 Du einen Server mit NodeJs betreibst, 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.

7.1.2.4 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-css --save-dev

Die Ablage mit –save-dev zeigt an, welche Werkzeuge zum Entwickeln 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).

7.1.2.5 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 Plug-In 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:

7.1.2.6 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.

7.2 Testen

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.

7.2.1 Testen mit Jasmine und Karma

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.

7.2.1.1 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:

$ npm install http-server jasmine-core karma karma-chrome-launcher k\
arma-coverage karma-jasmine remap-istanbul --save-dev
7.2.1.2 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:

7.2.1.3 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.

7.2.1.4 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/@angular/bundles/@angular-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.

7.2.1.5 Umgang mit Client-Bibliotheken

Komplexe Bibliotheken wie Angular erfordern Anpassungen. Für die Kompatiblität zwischen Karma und Angular benötigt man 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 und zeitnah 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.

7.2.2 Eine Beispiel-Applikation mit Tests

Die Applikation hier ist rein client-orientiert. Die möglichen serverseitigen Dienste spielen erstmal keine Rolle, um es nicht unnötig 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.ts
 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 }
7.2.2.1 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 hier 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 ein erwartetes Ergebnis und ein tatsächliches Resultat. 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 });
7.2.2.2 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

angular-testing@1.0.0 build /Applications/book/angular-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/book/angular-testing npm run build

angular2-testing@1.0.0 build /Applications/book/angular-testing rm -rf dist && tsc -p src

angular2-testing@1.0.0 test /Applications/book/angular-testing karma start karma.conf.js

angular-testing@1.0.0 posttest /Applications/book/angular-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?

7.2.2.3 Die Testabdeckung

Zuerst wird der Bericht erstellt:

1 $ npm run coverage

angular-testing@1.0.0 coverage /Applications/book/angular-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

angular-testing@1.0.0 start /Applications/book/angular-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

7.3 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 Systeme, aber WebPack ist führend und soll hier stellvertretend kurz vorgestellt werden.

7.3.1 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.

7.3.1.1 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:

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.

7.3.1.2 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:

7.3.1.3 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 Aufspalten 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.

7.3.1.4 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.

7.3.2 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:

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.

7.3.2.1 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.

7.3.2.2 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.

7.3.2.3 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:

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.

7.3.3 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:

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.

7.3.3.1 Node-Module

Node-Module können direkt importiert werden und WebPack kümmert sich um die Auflösung der Abhängigkeiten. Dazu wird der in Node geläufige Präfix “~” benutzt:

@import "~normalize.css";

Statt ständigem Kopieren von Bausteinen an die richtige Stelle wird jetzt npm die Verwaltung überlassen. CSS ist freilich nur ein Beispiel, normale Module sind ebenso nutzbar. Um hier Klarheit zu schaffen zuerst ein dediziertes Beispiel für CSS:

7.3.3.2 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 weiterentwickelt. (Quelle: Wikipedia)

Installiere zuerst SASS und die Ladefunktion:

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

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           'css-loader',
 9           'sass-loader',
10         ]
11       } 
12       // …
13     ],
14   },
15 };

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.

7.3.3.3 Abtrennen der CSS-Dateien

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

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

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

Installiere den neuen Loader:

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.

7.3.4 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.

7.3.5 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/

7.4 ECMSScript 2015 und Neuer

Wenn nicht TypeScript, sondern neuere JavaScript-Funktionen benutzt werden sollen, der Benutzer aber die Freiheit hat, ältere Browser zu benutzen, benötigst du einen Transpiler, der zwischen den Versionen vermittelnd übersetzt. Der beste ist derzeit Babel.

7.4.1 Babel

Google definiert Babel folgendermaßen:

Ba̱·bel, Substantiv [das] 1. ein Ort wilder Ausschweifungen. Synonyme: Sündenbabel 2. eine Weltstadt, in der viele Sprachen gesprochen werden.

Sehr erhellend ist das nicht. Tatsächlich hat aber Babel etwas mit Sprachen zu tun – mit Sünde allerdings eher weniger. JavaScript liegt in einer Vielzahl von Dialekten und Versionen vor. Browser sind mit ihrer Unterstützung auch sehr unterschiedlich. Das Problem löst ansich TypeScript, aber nicht immer ist der Sprung zu TypeScript gerechtfertigt. Gerade kleinere Applikationen und schlanke Bibliotheken wie React profitieren davon nur wenig und der Aufwand für die Infrastruktur steigt immens an.

Babel sagt: “Babel is a JavaScript compiler.”

Es wird also modernes, aktuelles, einheitliches JavaScript in ein solches übersetzt, dass jeder Browser versteht. Babel versteht auch so schicke Template-Sprachen wie JSX (über weitere Plug-Ins allerdings).

7.4.2 Installation

Die Installation erfolgt wie immer mit npm:

1 npm i --global --save-dev babel

Die neue Syntax von [ES2015}(#es2015) (ECMAScript 6) wird mit folgender Voreinstellung erreicht:

1 npm install --save-dev babel-preset-env

7.4.3 Babel nutzen

Ein Beispiel zur Nutzung von Babel findest du im Kapitel zu React.