Aus Linux-Magazin 01/2017

Das Client-seitige Javascript-Framework Angular 2

© Yana Gayvoronskaya, 123RF

Mit Typescript statt Javascript, JiT- und AoT-Kompilation und einem konsequenten Einsatz von Components brühen die Macher das Javascript-Framework Angular in Version 2 neu auf. Das Linux-Magazin probiert einen kräftigen Schluck von dem Gebräu.

Nach immerhin sechs Jahren Entwicklungszeit erschien Mitte September 2016 die zweite Major-Release des Client-seitigen Javascript-Frameworks Angular [1]. Die gute Nachricht: Auch Angular 2 setzt auf Databindings, um Anwendungsdaten im Browser über Direktiven, das sind eigens definierte HTML-Tags, im HTML-Dokument zu platzieren. Die schlechte: Angular 2 ist nicht rückwärtskompatibel. Das liegt aber weniger an dem Rewrite der Codebasis unter Typescript [2], sondern vielmehr an Design-Entscheidungen der Macher.

Der vorliegende Artikel beschreibt anhand einer Beispielanwendung (Abbildung 1), wie Entwickler Apps unter Angular 2 mit Typescript programmieren. Die App ist schlicht: Ein Klick auf »Add« überführt die beiden Messwerte zu Temperatur und Druck aus den Formularfeldern in die Tabelle am unteren Ende des Ausgabebereichs. Zeitgleich errechnet die App die Mittelwerte der gemessenen Werte in der Zeile unter der Überschrift »Wetterstation«.

Abbildung 1: Die Wetterstation erreicht der Anwender im Browser &uuml;ber die URL <code>http://localhost:3000</code> oder eine lokale IP-Adresse.

Abbildung 1: Die Wetterstation erreicht der Anwender im Browser über die URL »http://localhost:3000« oder eine lokale IP-Adresse.

Schnellstart

Zunächst installieren die folgenden zwei Zeilen eine aktuelle 6er Version der Server-seitigen Javascript-Implementierung Node [3] auf dem Entwicklungsrechner mit Debian 8:

curl -sL https://deb.nodesource.com/ setup_6.x | bash -apt install nodejs git

Zeile 1 erweitert die Sammlung von Paketquellen für Debians Paketmanager, indem sie ein Repository für die derzeit aktuelle Version 6.7.0 von Node ergänzt. Dann installiert Apt mit Rootrechten Node und Git, die der Anwender zur Installation von Angular 2 benötigt.

Eine Schnellstart-Variante der Version 2.1.0 von Angular holen die folgenden Befehle auf die Festplatte:

git clone https://github.com/angular/ quickstart station
cd station
npm install

Zeile 1 kopiert das begehrte Schnellstarter-Modul per Git in das Projektverzeichnis »station«. Der User wechselt anschließend dorthin und installiert das Modul über Nodes Paketmanager »npm«[4].Das Schnellstarter-Modul liefert eine Out of the Box lauffähige, aber recht rudimentäre Angular-2-Anwendung. Die Listings des Artikels erweitern dieses Modul um eine kleine Beispiel-App.

Testlauf

Listing 1 liefert aber zunächst einen Überblick: So sieht es im Projektverzeichnis der fertigen Beispielanwendung aus. Die zugehörigen Dateien liegen im Ordner »v2«, der User kopiert sie in den Ordner »station« und überschreibt so die dort vorhandenen Files. Die Datei »package.json« etwa speichert die Namen und Versionsnummern der benötigten Node-Pakete, die das Kommando »npm install« bereits installiert hat.

Listing 1

Das Projektverzeichnis station

01 - station
02 |- app
03  |- app.component.html
04  |- app.component.ts
05  |- app.module.ts
06  |- weather.ts
07  |- main.ts
08  |- stack.service.ts
09 |- index.html
10 |- package.json
11 |- systemjs.config.js
12 |- node_modules
13 |- styles.css
14 |- tsconfig.json
15 |- typings
16 |- typings.json

Das Verzeichnis »app« enthält vorrangig anwendungsspezifische Dateien wie »app.component.html«, »app.component.ts«, »weather.ts« oder »stack.service.ts«. Auch die Datei »app.module.ts« wartet hier, sie bindet die Anwendung zu einem Angular-Modul zusammen. Die Datei »tsconfig.json« erklärt dem Typescript-Compiler, wie er Javascript erzeugen soll.

Wechselt der Entwickler nun in das Verzeichnis »station« und startet die Entwicklungsumgebung mit

npm start

übersetzt der Compiler jede Datei mit der Endung ».ts« nach ».js«. Mitunter enthält noch die Datei »typing.json«[5] im gleichnamigen Verzeichnis Angaben zu bibliotheksspezifischen Erweiterungen, die der Typescript-Compiler nicht erfassen kann.

Hat die Entwicklungsumgebung die Dateien übersetzt, startet sie einen HTTP-Server und zeigt die zugehörigen URLs auf der Kommandozeile an. Der Entwickler erreicht die Wetterstation, indem er in die Browserleiste auf dem Entwicklungsrechner die URL »localhost:3000« eingibt (Abbildung 1). Läuft Angular in einer VM oder einem Container, setzt er deren IP-Adresse ein. Auf Port »3001« läuft parallel das Testtool Browsersync (Abbildung 2), das bei Bedarf Tests für die Webanwendung anbietet.

Abbildung 2: Wer seine Angular-2-App gleich im Browser testen m&ouml;chte, kann auf Browsersync zur&uuml;ckgreifen.

Abbildung 2: Wer seine Angular-2-App gleich im Browser testen möchte, kann auf Browsersync zurückgreifen.

Der Zeit voraus

Der Webbrowser greift zunächst über die »index.html« (Listing 2) auf die Anwendung zu. Zusätzliche Pakete für die Anwendung definiert die Datei »systemjs.config.js«. Intern nutzt Angular einen Compiler, der den Anwendungscode in einen interaktiven DOM-Baum umwandelt. Diesen so genannten View Source erzeugt er, wie Abbildung 3 zeigt, nicht nur Just in Time (JiT) im Browser, sondern bereits vorher auf dem Webserver, Ahead of Time (AoT, [6]).

Listing 2

index.html

01 <html>
02   <head>
03     <title>Wetterstation</title>
04     <script src="node_modules/core-js/client/shim.min.js"></script>
05     <script src="node_modules/zone.js/dist/zone.js"></script>
06     <script src="node_modules/reflect-metadata/Reflect.js"></script>
07     <script src="node_modules/systemjs/dist/system.src.js"></script>
08     <script src="systemjs.config.js"></script>
09     <script>
10       System.import('app').catch(function(err){ console.error(err); });
11     </script>
12     <link rel="stylesheet" href="styles.css">
13   </head>
14   <body>
15     <myApp>Laden ...</myApp>
16   </body>
17 </html>

Diese Methode spart Rechenzeit im produktiven Betrieb, in der User die Anwendung zigfach vom Webserver abrufen. Glaubt man den Machern von Angular 2, initialisiert eine Anwendung unter dem JiT-Compiler fast viermal schneller als unter Angular 1 [7].

Die eingebundene CSS-Datei »styles.css« speichert Formatierungsangaben unter CSS 3. Ein Watch-Prozess behält stets das Projektverzeichnis im Auge und veranlasst bei jeder Änderung, die ».ts«-Dateien neu zu kompilieren und die App im Browser selbsttätig neu zu laden.

Abbildung 3: Der AoT-Compiler (unten) generiert den View Source bereits auf dem Server, der JiT-Compiler (oben) erst im Browser. Die Pfeile beschreiben die Umwandlungsschritte.

Abbildung 3: Der AoT-Compiler (unten) generiert den View Source bereits auf dem Server, der JiT-Compiler (oben) erst im Browser. Die Pfeile beschreiben die Umwandlungsschritte.

Listing 2 zeigt die Datei »index.html« aus dem Wetterstation-Projekt. Die Anwendung verwendet die Originalversion aus dem Schnellstarter-Modul fast unverändert. Die Zeilen 4 bis 8 binden die benötigten Javascript-Ressourcen ein. Zeile 10 startet die App, indem sie die Datei »app/main.js« aufruft. Zeile 12 bindet das Stylesheet ein. Um die benutzerdefinierte Direktive »myApp« in Zeile 15 kümmert sich die Component aus Listing 4. Sie ersetzt »myApp« durch ein HTML-Fragment aus dem Template in Listing 3.

Sie ewig zu binden

Dieses Template steckt in der Datei »app/app.component.html«. Es verwendet die als Direktiven bezeichneten Spezial-Tags (etwa in Zeile 3), um Anwendungsdaten aus der Component zu ziehen.

Abbildung 4 wirft einen genaueren Blick auf die Binding-Mechanismen unter Angular 2. Der Bereich links unter »Template« listet die Direktiven auf. Sie verweisen entweder auf die Variable »val« oder die Callback-Funktion »chg()« aus der »Component« rechts.

Abbildung 4: Die Binding-Mechanismen zwischen Template und Component unter Angular 2.

Abbildung 4: Die Binding-Mechanismen zwischen Template und Component unter Angular 2.

Die Pfeilrichtungen zeigen die Wege der geänderten Werte und Zustände an. Speichert die Component etwa den Wert 3 in »val«, ändert sich der Ausdruck »{{val}}« im Template auch auf 3. Gleiches gilt für die Textfelder, die die Attribut-Direktiven in der Form »[ngModel]=”val”« oder »[(ngModel)]=”val”« tragen.

Rückgabewerte von Funktionen aktualisiert Angular 2 ebenfalls. Das zeigt etwa Zeile 3 in Listing 3. Sie aktualisiert die mit der Funktion »avg()« berechneten Mittelwerte von Temperatur und Druck (Abbildung 1), sobald die App einen weiteren Satz an Messwerten speichert.

Die neue Attribut-Direktive »ngModel« bietet im Gegensatz zu ihrem Vorgänger »ng-model« (man beachte den Bindestrich) auch One-Way-Bindings in Form von »[ngModel]« und »(ngModel)«. Die Kombination »[(ngModel)]« vereinbart für die beiden Formularfelder in den Zeilen 5 und 6 jeweils Two-Way-Bindings wie unter Angular 1. Eventuell haben die kritischen Diskussionen um Two-Way-Bindings die Macher von Angular dazu bewogen, wie Konkurrent React [8] One-Way-Bindings nachzurüsten.

Zeile 7 von Listing 3 setzt runde Klammern ein, um die Callback-Funktion »add()« in der Component aufzurufen. Die in Angular 1 verwendete Direktive »ng-submit« ist Geschichte. Um die Arbeit von »ng-repeat« kümmert sich nun »ngFor« (Zeile 15). Der Sternoperator wandelt das markierte »tr«-Element zusammen mit seinen Kindelementen in ein Master-Template um, das die App pro Listenelement einmalig gegen den Wert der Laufvariablen aufrechnet.

Listing 3

app/app.component.html

01 <div id="station">
02   <h1>Wetterstation</h1>
03   <div id="avg">Durchschnitt: {{avg('temp')}} °C und {{avg('press')}} mBar</div>
04   <form>
05     <input type="number" [(ngModel)]="model.temp" name="temp" required step="0.1" size="30" placeholder="°C">
06     <input type="number" [(ngModel)]="model.press" name="press" required size="30" placeholder="mBar">
07     <button (click)="add()" value="add">Add</button>
08   </form>
09   <table>
10   <tr>
11     <th>Datum</th>
12     <th>Temperatur</th>
13     <th>Druck</th>
14   </tr>
15   <tr *ngFor="let data of datas">
16     <td>{{data.date | date:'dd.MM.yyyy HH:mm'}}</td>
17     <td>{{data.temp}}</td>
18     <td>{{data.press}}</td>
19     </tr>
20   </table>
21 </div>

Im Wert der Attribut-Direktive »ngFor« deklariert das Schlüsselwort »let« die Laufvariable »data«, der Operator »of« iteriert über die Werte der Liste »datas«.

Um die Werte der Laufvariablen »data« zu extrahieren, nutzen die Zeilen 16 bis 18 den Interpolations-Operator (»{{…}}«). Die erste Zeile reicht den Wert der Komponente »date« per Pipe (»|«) an die Direktive »date« weiter und formatiert das Datum Locale-abhängig.

Komponentenkleber

Ursprünglich sollte Angular 2 ECMA-Script 6 unterstützen, den aktuellen Sprachstandard für Javascript. Nun übernimmt Typescript diese Rolle, eine von Microsoft erdachte Obermenge von Javascript. Das ergänzt die Ausdrucksstärke und Robustheit von ECMA-Script durch Sprachmittel und Plausibilitätsprüfungen. Tabelle 1 vergleicht Typescript mit den relevanten Standards von ECMA-Script und legt einen Fokus auf die Sprachmittel von Angular 2. Sie zeigt auch, wie tief in die Trickkiste Typescript greifen muss, um eine Anwendung nach ECMA-Script 5 zu übersetzen.

Tabelle 1
Vergleich von Sprachmitteln
Sprachmittel ECMA-Script 5 ECMA-Script 6 ECMA-Script 7 Typescript
Statische Typen ja
Klassen ja ja ja
Interfaces ja
Dekoratoren ja ja
Observablen ja ja

Aufgrund der damit verbundenen Vorteile sollten Angular-2-Anwender künftig besser Typescript verwenden, wenn sie neue Apps schreiben. Alternativ greifen sie zu Javascript oder auch zu Googles Programmiersprache Dart [9].

Listing 4 zeigt den Handler aus der Datei »app.componenten.ts«. Er kümmert sich um die Direktive »myApp« (Listing 2) und ist als Component umgesetzt. Die zweite Zeile importiert den Dekorator »Component« und das Interface »OnInit« aus Angulars Core-Paket, die nächste Zeile holt die Klassen »StackService« und »Weather« aus den Dateien »stack.service.ts« (Listing 5) und »weather.ts« (Listing 6).

Listing 4

app/app.component.ts

01 // Die Component ersetzt die Direktive <myApp> durch die eigentliche Anwendung
02 import { Component, OnInit } from '@angular/core';
03 import { Weather }      from './weather';
04 import { StackService } from './stack.service';
05
06 @Component({
07   selector: 'myApp',
08   templateUrl: 'app/app.component.html',
09 })
10 export class AppComponent implements OnInit {
11   datas: Weather[] = [];
12   model: Weather = new Weather(8, 1080);
13   constructor(private stackService: StackService) {}
14   avg(field: string): number { return this.stackService.avg(field); };
15   add(): void { this.stackService.add(new Weather(this.model.temp, this.model.press)); };
16   ngOnInit(): void { this.datas = this.stackService.datas}
17 }

Der Code legt die Component »AppComponent« als Klasse an. Dazu versieht der Dekorator »@Component« zunächst die Klassendefinition (Zeile 10) mit den Eigenschaften einer Component, wozu die beiden Konfigurationsobjekte dienen. In Zeile 7 wählt »selector« die benutzerdefinierte Direktive »myApp«, die auch in der »index.html« steckt (Listing 2), als Betätigungsfeld aus, »templateUrl« verlinkt das Template eine Zeile später mit der HTML-Datei aus Listing 3.

Anders als sein Vorgänger erlaubt Angular 2 Templates nur noch in Verbindung mit Components. Die Typendeklaration einer Variablen erfolgt unter Typescript nach dem Doppelpunkt (Listing 4, Zeile 11). So nimmt »datas« eine Liste des benutzerdefinierten Typs »Weather« aus Listing 6 auf und speichert alle Sätze von Messwerten. Die Variable »model« ist hingegen einfach vom Typ »Weather« und dient dazu, über die Direktive »[(ngModel)]« Messwerte aus ihren Formularfeldern (Listing 3, Zeilen 5 und 6) auszulesen. Im Übrigen reicht bereits ein einfaches »(ngModel)« zum Lesen aus.

Die Component implementiert nebenbei noch das Interface »OnInit«. Klassen, die es implementieren, nehmen Dienste wie »StackService« auf. Praktischerweise kann Angular Dienste als Singleton [10] erzeugen. Dann verteilt es sie – wie Components oder Pipes – über die Konstruktorfunktion der Direktiven (Zeile 13). Der Handler »ngOnInit()« verlinkt dann die Liste »datas« mit der gleichnamigen Komponente aus dem Objekt vom Typ »StackService«. Die Methoden »avg()« und »add()« umhüllen die namensgleichen Methoden aus »StackService«.

Injektion

Listing 5 zeigt den Dienst »StackService« – einen einfachen Datenspeicher aus der Datei »app/stack.service.ts«. Zeile 2 importiert zunächst den Dekorator »Injectable«. Dieser macht die Klasse »StackService« injizierbar. Die Anwendungsdaten speichert die Liste »datas« in Zeile 6. Der Datentyp »any« garantiert einen universell verwendbaren Datenspeicher, da »any« jeden Datentyp akzeptiert.

Die Methode »add()« dient als Schnittstelle zum Speichern neuer Elemente. Eine Zeile darunter bildet »avg()« für die im Aufrufparameter »fld« übergebene Komponente den arithmetischen Mittelwert aller gespeicherten Objekte.

Listing 5

app/stack.service.ts

01 // Dienstklasse zum Speichern von Anwendungsdaten
02 import { Injectable } from '@angular/core';
03
04 @Injectable()
05 export class StackService {
06   public datas: any[] = [];
07   add(obj: any): void { this.datas.push(obj); };
08   avg(fld: string): number {
09     if (this.datas.length > 0) {
10       return this.rnd(this.datas.reduce((s, o) => s + parseFloat(o[fld]), 0)/this.datas.length);
11     };
12     return 0;
13   };
14   rnd(val: number, digits: number = 2) {
15     let pre = Math.pow(10, digits);
16     return Math.round(val*pre)/pre;
17   };
18 }

Ist die Liste nicht leer, summiert die Methode »reduce()« über die Komponente alle gespeicherten Objekte. Die Summe dividiert sie dann durch die Länge der Liste (Zeile 10). Dann rundet »rnd()« den errechneten Mittelwert auf zwei Stellen hinter dem Komma. Für die leere Liste gibt »avg()« hier zu Demozwecken 0 zurück.

Die Hilfsmethode »rnd()« übernimmt in Zeile 14 in »val« die zu rundende Zahl und in »digits« die Anzahl der Nachkommastellen. Die Zeile darunter bildet die Potenz 10digits. Die letzte Zeile rettet die erwünschten Nachkommastellen durch Multiplikation zunächst vor dem Runden, um sie dann wieder per Division hinter das Komma zu schieben.

Finetuning

Listing 6 zeigt die Klasse Weather aus der Datei »app/weather.ts«. Sie speichert die Messwerte (Zeilen 4 und 5). Weather lässt sich wie »number« oder »any« als Datentyp verwenden. Die Konstruktor-Funktion ab Zeile 6 bindet die übergebenen Aufrufparameter an gleichnamige Attribute der Instanz. Das Fragezeichen hinter »date« macht die Angabe optional, ohne erzeugt Zeile 9 den aktuellen Zeitstempel.

Listing 6

app/weather.ts

01 //  Weather lässt sich auch als Datentyp in einer Typdeklaration verwenden
02 export class Weather {
03   date: string;
04   temp: number;
05   press: number;
06   constructor(temp: number, press: number, date?: string) {
07     this.temp = temp;
08     this.press = press;
09     this.date = (date)?date:(new Date()).toISOString();
10   }
11 };

Vor dem finalen Praxistest ist noch die fertige Anwendung in ein Angular-Modul zu verwandeln. Diesen Job übernimmt die Datei »app.module.ts« (Listing 7). Sie importiert zunächst den Dekorator »NgModule«, dann die Direktiven aus »BrowserModule« und »FormsModule« und die Klassen aus den Listings 4 und 5.

Listing 7

app/app.module.ts

01 // App zu einem Angular-Modul zusammenfassen
02 import { NgModule } from '@angular/core';
03 import { BrowserModule } from '@angular/platform-browser';
04 import { FormsModule } from '@angular/forms';
05 import { AppComponent } from './app.component';
06 import { StackService } from './stack.service';
07
08 @NgModule({
09   imports:      [ BrowserModule, FormsModule],
10   declarations: [ AppComponent ],
11   bootstrap:    [ AppComponent ],
12   providers:    [ StackService]
13 })
14 export class AppModule { }

Ab Zeile 8 von Listing 7 formt der Dekorator »NgModule« aus der am Anfang noch leeren Klassendefinition in Zeile 14 ein passendes Angular-Modul. Bei dieser Gelegenheit vererbt der Dekorator zugleich auch alle eingebauten Direktiven modulweit.

Weitere Direktiven importiert der Code in Zeile 9 in die App. So liefert »BrowserModule« beispielsweise die Direktiven »ngFor« und »ngIf«, während »FormsModule« die Direktive »ngModel« holt. Eine Zeile darunter sammelt »declarations« sämtliche benutzerdefinierten Direktiven ein – wozu Components, Pipes und sonstige gehören.

Zeile 11 legt schließlich mit »bootstrap« die »AppComponent« als Wurzel-Komponente fest, welche die App beim Start aufruft. Schließlich instanziiert und verteilt »providers« Dienste, hier »StackService«, über die benutzerdefinierten Direktiven. Speichert der Entwickler das letzte Listing selbsttätig, aktualisiert sich die Anwendung direkt im Browser (Abbildung 5).

Abbildung 5: Ein HTTP-Server liefert die Angular-2-Anwendung aus, auf die der User per Browser zugreift.

Abbildung 5: Ein HTTP-Server liefert die Angular-2-Anwendung aus, auf die der User per Browser zugreift.

Fazit

Angular 2 punktet mit flexibleren Databindings und setzt den Fokus dabei mehr als zuvor auf Components. Zugleich beschleunigt der neue Compiler das Framework mit Hilfe von JiT oder AoT. Typescript hievt den Code des Frameworks sowie der darin geschriebenen Anwendungen auf einen zeitgemäßen Stand der Programmierkunst – die native Umsetzung gängiger Sprachmittel in den Browsern hinkt dagegen oft Jahre hinterher. In der vorliegenden, modernistischen Form dürfte Angular 2 viele neue Freunde finden, nur eine umfassende Sammlung von UI-Widgets, wie sie etwa Ext.js [11] bietet, trennt Entwickler jetzt noch vom großen Glück.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 5 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Nach oben