Über unsMediaKontaktImpressum
Martin Reinhardt 12. September 2017

Angular 4 – beyond the web...

Viele der alten AngularJS 1-Konstrukte sind heute schlichtweg nicht mehr nötig. © Angular.io
© Angular.io

AngularJS (Angular 1) ist im Jahr 2009 erschienen. Das Framework war schnell in aller Munde. Zum damaligen Zeitpunkt war die Frontend-Welt geprägt von Seiten, die mit jQuery gebaut und oft leider auch verbaut wurden. Wichtigste Eigenschaften von AngularJS sind die Zwei-Wege-Datenbindung zum Abgleich von Darstellung und Daten, und Dependency zwischen verschiedenen Komponenten. Dabei prägt AngularJS den Begriff der Single-Page-Anwendungen (SPA) wie kein anderes Framework.

Eine SPA besteht aus einem einzigen HTML-Dokument und die Inhalte werden dynamisch nachgeladen. Diese Art von Web-Architektur steht im Gegensatz zu klassischen Webanwendungen, welche aus mehreren, untereinander verlinkten HTML-Dokumenten bestehen. Eine Einzelseiten-Webanwendung ermöglicht eine Reduzierung der Serverlast, gibt dem Anwender das Gefühl einer klassischen Desktopanwendung und erlauben die Umsetzung von selbständigen Webclients, die auch temporär offline verwendbar sind. Insbesondere aus letzterem Grund eignen sie sich für den Einsatz in störanfälligen und oft unterbrochenen Verbindungen mobiler Endgeräte. Seit kurzem ist Angular 2 verfügbar und bricht an vielen Stellen mit der Version 1. Dabei soll vor allem dem Umstand Rechnung getragen werden, dass Anwendungen größer geworden sind und die Browser deutlich leistungsfähiger als das noch vor 10 Jahren der Fall war.

Die Frontend-Welt ist teilweise schon verrückt: Man hat das Gefühl, dass ständig neue Frameworks und Tools entstehen. Erst ist Angular 2 erschienen und jetzt im März 2017 Angular 4. Und wo ist Angular 3?

Das Angular-Projekt setzt auf das System der semantischen Versionierung (SEMVER). Hierbei geht es darum, den Versionsnummer eine wirkliche Bedeutung zu geben. Dies soll Entwickler dabei unterstützen, besser mit Updates umzugehen.

Abb.1: SEMVER besteht aus 3 Teilen. © Martin Reinhardt
Abb.1: SEMVER besteht aus 3 Teilen. © Martin Reinhardt

SEMVER besteht aus 3 Teilen (s. Abb.1).

Auf Grundlage einer Versionsnummer von MAJOR.MINOR.PATCH werden die einzelnen Elemente folgendermaßen erhöht:

  • MAJOR wird erhöht, wenn API-inkompatible Änderungen veröffentlicht werden, 

  • MINOR wird erhöht, wenn neue Funktionalitäten, welche kompatibel zur bisherigen API sind, veröffentlicht werden, und 

  • PATCH wird erhöht, wenn die Änderungen ausschließlich API-kompatible Bugfixes umfassen. Außerdem sind Bezeichner für Vorveröffentlichungen und Build-Metadaten als Erweiterungen zum MAJOR.MINOR.PATCH-Format verfügbar. 


Wenn nun neue Features hinzu kommen, garantiert uns die Erhöhung der Minor-Version ebenfalls, dass unsere Anwendungen auf Angular in Version 2 nicht brechen werden. Hier können wir entscheiden, ob wir dieses Update einspielen wollen oder nicht – je nachdem, ob wir die neues Features nutzen wollen. Patch- und Minor-Updates können und sollen also ohne das Eingreifen in Ihren Quellcode möglich sein. Wichtig für uns ist hierbei immer nur der Sprung der Major-Version. Dieser zeigt an, dass wir als Entwickler beim Update auf eine höhere Version unseren geschriebenen Quellcode anpassen müssen. Dies kann z. B. durch die Umbenennung einer Core-Direktive oder auch einer Service-API kommen. Was vollkommen okay ist – Software, die am Puls der Zeit bleiben will, muss irgendwann Breaking-Changes in Kauf nehmen, sonst bleiben wir in der Vergangenheit hängen. Ein Beispiel hierfür ist der Wechsel von TypeScript Version 1.8 auf Version 2.2. Da hierbei einige Features von TypeScript nicht mehr verfügbar sind, ist dies ein Breaking-Change – auch wenn es nur eine Abhängigkeit von Angular selbst ist.

Die meisten Nutzer haben heute mehr Leistung ständig abrufbar in der Hosentasche als Hochleistungsrechner vor 10 Jahren.

Alle Angular-Pakete befinden sich in einem Git-Repository. Alle Core-Pakete sind in der Major-Version 2, nur der Router ist in Version 3. Um dies zu beheben, wird der nächste Breaking Change alle Versionen auf Major-Version 4 ziehen, damit werden alle Pakete innerhalb von Angular ab diesem Zeitpunkt einheitlich nach SEMVER behandelt. Damit soll in Zukunft sichergestellt werden, dass Upgrades stabiler ablaufen und die Versionsnummer eben eine echte Semantik hat.

Die generellen Gründe für einen großräumigen Reboot von AngularJS waren verschiedene Faktoren:

  • Browserentwicklung
  • ECMAScript 6

  • Web Components
  • Performance
  • Mobile
  • Wartbarkeit/Refactoring

Gerade die Punkte Performance und Wartbarkeit werden umso klarer, je mehr man sich mit der Geschichte von JavaScript und auch Angular beschäftigt:

  • 1998: ECMAScript 2
  • 1999: ECMAScript 3
  • 2000 - 2009: ECMAScript 4 auf und ab
    – sehr fortschrittlicher Ansatz
    – u. a. Klassen und deklarierte Typen
    – ActionScript 3 als Implementierung
  • 2005: Ajax und damit SPAs kommen auf
    – Renaissance der Sprache JavaScript
    – Bibliotheken: jQuery, Dojo u.a.
  • 2007: Als Gegenbewegung zu ECMAScript 4 bringen Microsoft und Yahoo ECMAScript 3.1 auf den Weg

AngularJS 1 ist im Jahr 2009 herausgekommen und hat damit auch mittlerweile 8 Jahre auf dem Buckel. In dieser Zeit hat sich die Leistungsfähigkeit der Webbrowser und auch der mobilen Geräte dramatisch gesteigert. Die meisten Nutzer haben heute mehr Leistung ständig abrufbar in der Hosentasche als Hochleistungsrechner vor 10 Jahren. AngularJS 1 war gebaut worden, um es Entwicklern zu ermöglichen, schnell kleine Anwendungen im Webbrowser zu entwickeln und sich dabei möglichst wenig mit den jeweiligen Eigenheiten der Webbrowser auseinanderzusetzen. Dabei sind auch Schichten eingezogen worden die heute einfach nicht mehr benötigt werden. Viele der alten AngularJS 1-Konstrukte sind heute schlichtweg nicht mehr nötig.

Insofern war die Entscheidung, einen neuen Rewrite und damit eine echte neue Hauptversion zu starten, nur logisch. Folgende AngularJS 1.x Konstrukte finden wir in Angular 2 gar nicht mehr:

  • $scope
  • HTML template syntax
  • Dirty checking

  • Controllers

  • angular.module

  • jqLite

  • Interceptoren

Dafür gibt es jede Menge neuer Framework-Elemente:

  • Object.observe hybrid

  • ECMAScript 6 / TypeScript

  • Komponenten und Modulsystem
  • Reactive Programmierung (RxJs)

Aber einige Elemente aus der alten AngularJS-Version haben es auch in die neue Version geschafft: 

  • Services
  • Direktive
  • Dependency Injection MVC

Für einige Konstrukte, z. B. Interceptoren, gibt es mittlerweile Bibliotheken, die sich dem Thema angenommen haben.

Komponenten überall

Das 2015 eingeführte Angular 2 führte ein Vielzahl wiederverwendbarer Elemente ein. Damit wird wie schon in AngularJS die Möglichkeit geschaffen, dass Dritthersteller Erweiterungen schnell und bequem Funktionen für verschiedene Anwendungsfälle erstellen können.

Trotz der Unterschiede ist eine schrittweise Migration möglich, da man z. B. ein Downgrade von Angular-Komponenten zu AngularJS durchführen kann. In die andere Richtung ist auch ein Upgrade möglich. Das wird möglich, da einige Features von Angular in die AngularJS neu übernommen werden, wie z. B. die Komponenten. Angular ist aber mittlerweile nicht mehr auf den Webbrowser beschränkt. Das neue Framework ist in viele kleine Module zerlegt und erlaubt auch den Austausch des Renderers. So gibt es seit Angular 2 auch eine Server-Implementierung, die "Angular Universal" genannt wird.

Dennoch bleiben viele Konzepte gleich, nur die jeweilige Entsprechung im Framework heißt nun anders und setzt in vielen Fällen auf Standard-Elementen aus dem EcmaScript-Standardisierungsprozess auf:

  • Wiederverwendbare Komponenten
  • Module
  • Routing

  • Pipes
  • Properties
  • Angular 2 nicht beschränkt auf (Web-)Browser
  • Server-Implementierung

  • Webbrowser

  • Einfach zu lernen und zu nutzen

  • Migration von AngularJS 1.x möglich

  • Architekturkonzepte

Das Erstellen von Komponenten geht schnell von der Hand:

import { Component } from '@angular/core';

@Component({
  selector: 'my-component',
  template: `  Hello my name is {{name}}.
                <button (click)="sayMyName()">Say my name</button>
`
})
export class MyComponent {
  constructor() {
    this.name = 'Max'
  }
  sayMyName() {
    console.log('My name is', this.name)
}
}

Template bindings

Letzlich ist es eine Typescript-Klasse mit derm Angular-Dekorator @Component. Das HTML wird per ES6-Template Syntax eingebunden und zeigt einige typische Bindings von Angular, wie click. Daneben gibt es weitere Bindungs:

  • {{ }} – Interpolation, z. B. Textausgabe

  • [] – Property Binding, z. B. CSS-Änderungen

  • () – Event Binding, z. B. Eingabe-Events wie Klicken
  • # – Variable Declaration
  • * – structural Directives

In den Bindings sind auch Verschachtelungen möglich, wie z. B. in CSS: style.width.px. Zur weiteren Erläuterung hier einige Beispiele:

// Event Handling
<button (click)="read($event)">

// Two-Way-Binding
<my-cmp [(title)]="name">
IT-Tage 2017 - Web, App, JavaScript

EventBus für unterwegs

Um Komponenten Daten zu übergeben, reicht es, mit Event Bindings zu arbeiten. Nimmt man beispielsweise eine Nutzerprofil-Komponente und bindet diese in einem Einstellungsbereich ein. Um auf Änderungen am Nutzerprofil reagieren zu können, reicht es aus mit einem Decorator einen Event-Emitter zu markieren. Über eine Art EventBus werden dann Änderungen von der Nutzerprofil-Komponente bekannt gemacht: Um Komponenten Daten zu übergeben, reicht Interpolation vollkommen aus:

import { Component, Input } from '@angular/core';

@Component({
  selector: 'user-profile',
  template: `
{{user.name}}
`
})
export class UserProfile {
  @Input() user;
  constructor() {}
}

Gerade bei der Wiederverwendung von Komponenten ist Event Binding sehr nützlich. Die Nutzerprofil-Komponente sendet also ein Event, sobald sich der Nutzer ändert:

import { Component, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'user-profile',
  template: `Profile of {{user.name}}`
})
export class UserProfile {
  @Output() userupdated = new EventEmitter();
  constructor() {
    // Update user
    // ...
    this.userUpdated.emit(this.user);
}
}

In dem Bereich, der die Nutzerprofil-Komponente einbindet, wird nun auf das Event reagiert (userUpdated):

<user-profile (userupdated)="userUpdated($event)"></user-profile>
  export class SettingsPage {
  constructor(){}

  userUpdated(user) {
    // Handle the event
}
}

Services neu aufgelegt

Einen alten Bekannten findet man bei den Services. Mit Angular wird dies sogar noch einfacher. Eigentlich sind Services in Angular 2 und 4 nichts weiter als eine TypeScript-Klasse, die mit dem Decorator @Injectable versehen wird:

import { Injectable } from '@angular/core';
import { User } from '@common/domain';

@Injectable()
export class UserService {
    getUsers():User {
        ...
}
}

Das Modulsystem und dessen Hierarchie sorgen dann für eine Initialisierung der Klasse zur Laufzeit. Services werden einfach im Konstruktor von anderen Komponenten als Parameter injiziert und stehen dann einfach zur Verfügung.

@Component({
    selector: 'user-root',
    providers: [UserService],
    template: `
    Anzahl an Nutzern: {{users.length}}
    `
})
export class AppComponent {
    public users = User[];
   
constructor(private userService: UserService) {
        this.users = this.userService.getUsers();
}
}

Services lassen sich wie in AngularJS 1 in Komponenten injecten. Dabei werden Services als Singelton pro Modul instanziiert. Der private Constructor-Parameter von TypeScript ist dabei sehr nützlich. Private Übergabeparameter im Constructor werden dabei automatisch Instanz-Variablen. Generell empfiehlt es sich bei komplexeren Anwendungen, nur Interfaces zu injecten. Damit können ggf. Services einfacher ausgetauscht werden:

import { Injectable } from '@angular/core';
import { SettingsService } from '@common/settings';

@Injectable()
export class WebSettingsService implements SettingsService {

...

  getString(key: string): string {
    return sessionStorage.getItem(key);
}

  setString(key: string, value: string): void {
    sessionStorage.setItem(key, value);
}

  removeString(key: string): void {
    sessionStorage.removeItem(key);
}
...
}

Module überall

Auch in der neuen Auflage von Angular existiert wieder ein ausgefeiltes Modulsystem. Damit ist es auch möglich, Bibliotheken zu erstellen und einfach in eigenen Anwendungen einzubinden. Das Angular-Team nutzt dasselbe System auch intern, z. B. Für das HttpModule:

import { HttpModule } from '@angular/http';

@NgModule({

// ...

  imports: [
    BrowserModule,
    FormsModule,
    HttpModule
]

// ...
})
export class AppModule { }

Module haben dabei einen eigenen Scope, d. h. Services werden immer als Singeltons pro Modul erzeugt. Es können aber auch Hierarchien von Modulen aufgebaut werden, um Services nicht mehrfach zu erzeugen. Denn Module in Angular müssen nicht immer einen rein technischen Schnitt erfahren [1]. Es ist auch möglich, Feature-Module zu erstellen, um z. B. abhängig von der Nutzerrolle andere Funktionalitäten bereitzustellen. Damit Services als Singeltons in der gesamten Anwendung nur einmalig vorkommen, bietet Angular für Module die forRoot-Methode an. Das entsprechende importierte Modul muss dazu angepasst werden:

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    HttpModule
  ],
  declarations: [
    UserComponent
  ],
  providers: [  ],
  exports: [
    UserComponent
  ]
})
export class LibraryModule {
  static forRoot(): ModuleWithProviders {
    return {
      ngModule: LibraryModule,
      providers: [
        UserService
      ]
};
}
}

Nun kann dieses Modul im AppModule des Konsumenten registriert werden:

@NgModule({
  imports: [
BrowserModule,
FormsModule,

HttpModule,
LibraryModule.forRoot(),
[...]
],
[...]
})
export class AppModule {
}

Über die forRoot-Variante werden auch die Services innerhalb des AppModules registriert und die Feature-Module importieren nun lediglich das Modul:

  imports: [
LibraryModule,
[...]
],
[...]
})
export class UserFeatureModule {
}

Durch dieses Modell können alle anderen Module des Konsumenten direkt importieren.

Component Lifecycle

Eine Komponente in Angular 2 durchläuft verschiedene Zustände während der Ausführung. Diese werden auch Lebenszyklen genannt. Über die Lifecycle-Hooks kann an verschiedenen Stellen eingegriffen werden. Folgende Funktionen können dazu genutzt werden:

  • ngOnInit – Komponente wird initialisiert (nach erstem ngOnChanges – Eigenschaften initialisiert)
  • ngOnDestroy – bevor Komponente zerstört wird
  • ngDoCheck – eigene Änderungserkennung

  • ngOnChanges(changes) – Änderungen in Bindings wurden erkannt
  • ngAfterContentInit – Inhalt wurde initialisiert

  • ngAfterContentChecked – jedes Mal, wenn Inhalt überprüft wurde
  • ngAfterViewInit – Views wurden initialisiert

  • ngAfterViewChecked – jedes Mal, wenn Views überprüft wurden

Die Implementierung der Lifecycle-Hooks in eigenen Komponenten ist denkbar einfach:

import { Component, OnInit } from '@angular/core';
import { UserService, User } from '@common';

...
export class AppComponent implements OnInit {

    constructor(private userService: UserService) {
    }

    ngOnInit() {
        this.userService
.getUsers()
          .subscribe((users: Array<user>) => this.users = users);
    }
}

Durch die Implementierung des entsprechenden Interfaces wird eine Schnittstelle in der Komponente bereitgestellt, die dann von Angular aufgerufen wird. Im oberen Beispiel wird, nachdem die Komponente initialisert wurde, eine Nutzerliste nachgeladen.

Angular CLI

Für den schnellen Einstieg in Angular eignet sich vor allem das Angular CLI.
Neben einer generierten Projektstruktur bietet das CLI auch Scaffolding-Kommandos. Das erleichtert gerade beim Einstieg das schnelle Vorankommen.

Scaffold Kommando
Component ng g component my-new-component
Directive ng g directive my-new-directive
Pipe ng g pipe my-new-pipe
Service ng g service my-new-service
Class ng g class my-new-class
Interface ng g interface my-new-interface
Enum ng g enum my-new-enum
Module ng g module my-module

App gehts...

NativeScript ist ein Open Source-Framework zur Entwicklung mobiler Apps mit JavaScript, TypeScript oder Angular. Dabei werden Android und iOS unterstützt. Gegenüber regulären hybriden Ansätzen wie Cordova verwendet NativeScript native UI-Elemente der jeweiligen Plattform, was sich auch in einer besseren Performance und angepasstem Look-and-Feel niederschlägt.

Gerade bei Verwendung von Angular kann aber auch Code einer Webanwendung geteilt werden (s.Abb.2).

Abb.2: NativeScript Groceries, Beispiel. © Martin Reinhardt
Abb.2: NativeScript Groceries, Beispiel. © Martin Reinhardt

Bei dem Groceries-Beispiel wird die WebApp nicht zusammen mit der mobilen App entwickelt. Man kann es noch weitertreiben. Einige Komponenten, wie Direktiven und Services, können für eine Angular-WebApp und Angular-NativeScript-Mobile-App problemlos geteilt werden. Damit kann ein Großteil der Funktionalität schnell auf allen wichtigen Platform zur Verfügung stehen (s.Abb.3).

Abb.3: NativeScript Advanced-Beispiele, Quelle: Nathan Walker
Abb.3: NativeScript Advanced-Beispiele, Quelle: Nathan Walker

Das obere Beispiel bringt aber auch eine gewisse Komplexität mit sich und ist deswegen nicht als Einstieg geeignet. Wer das Beispiel selbst ausprobieren möchte, kann anhand eines GitHub-Beispiels [3] die einzelnen Varianten selbst ausprobieren:

  • Desktop (npm run start.desktop)
  • Web (npm run start.deving)

  • App (npm run start.ios)

In Verbindung mit NativeScript zeigt sich eine Stärke von Angular: Das Framework ist flexibel, um auch andere Plattformen als das Web zu bedienen. Auch wenn die Unterstützung für native mobile Anwendungen Fluch und Segen zugleich ist, bietet es doch den Vorteil, dass man Erfahrungen und Wissen aus dem Web auch dort nutzen kann. Dennoch ist die Lernkurve hoch. Schließlich muss man sich mit den nativen Abhängigkeiten auseinandersetzen und auch die Anforderungen der mobilen Plattformen stärker berücksichtigen. Gerade die Verwendung von TypeScript erleichtert aber den Entwicklungsworkflow massiv. Fehler können früh erkannt werden und es gibt sogar Code-Genierungstools um aus Java-Klassen TypeScript-Klassen zu erstellen [4]. Das ist vor allem für Contract-Driven-Development auf REST-Basis sinnvoll. TypeScript öffnet bereits jetzt für viele PHP- und Java-Entwickler die Tür in die JavaScript-Welt. Die klare Semantik, weniger implizite Magie und dafür deklarative Konfiguration über Decorators helfen dabei.

nach Oben
Autor

Martin Reinhardt

Martin Reinhardt beschäftigt sich mit der Architektur von komplexen verteilten Systemen, modernen Webarchitekturen und Build Management. Er arbeitet als Architekt bei der Management- und IT-Unternehmensberatung Holisticon AG.
>> Weiterlesen
botMessage_toctoc_comments_929