Single Page Application

Bisher: Server rendert HTML und liefert komplette Seiten an den Client aus
Aufgeweicht durch AJAX: Auslieferung auch von partiellen Seiten
Neu: Auslieferung eines Programms zur Darstellung der Seite
Enthält auch HTML-Code, aber vor allem Darstellungslogik
Zur Laufzeit: Ausschließlich Übertragung von Daten zwischen Server und Client
Kein Versand von serverseitig berechnetem HTML-Code

Beteiligte Programme und Bibliotheken

Generisch: Webserver liefert statische Inhalte
Typische Serveranwendungen wie Apache 2, nginx, lighttpd, …
Speziell: Web-Applikation zur Verarbeitung von applikationsspezfischen Anfragen
Speziell: Clientanwendung zur Darstellung
  • Fragt Daten vom Server ab und visualisiert diese
  • Häufig, aber nicht notwendigerweise, im Browser
  • Häufig, aber nicht notwendigerweise, auf Basis einer JavaScript-Bibliothek

Prinzip der Webservices

Maschinenkommunikation über HTTP in gut serialisierbaren Austauschformaten
  • Identifizierung von Ressourcen über URL
  • Antwort als JSON, XML, CSV oder weiteren Standardformaten
Vielzahl unterschiedlicher Standards für Beschreibung von Schnittstellen
  • Web Services Description Language (WSDL) als Meta-Sprache zur Beschreibung von Datentypen und Funktionen (mehr dazu im XML-Kapitel)
  • Swagger als Beschreibungssprache für verfügbare HTTP-Endpunkte auf einem Server
Vorteil von formalen Schnittstellenbeschreibungen: Codegenerierung
  • Generierung von Client-Bibliotheken in beliebigen Programmiersprachen
  • Generierung von Server-Skeletten in beliebigen Programmiersprachen
Zugriffsmethoden & Programmierparadigmen
  • Remote Procedure Calls (RPC), Aufruf von entfernten Funktionen als wären diese lokal
  • Simple Object Access Protocol (SOAP)
  • Representational State Transfer (REST)

Architektur für Single Page Applications

Anfragen und Abläufe, (Quelle: )

Schichtenarchitekturen

Zwei-Schichten-Architektur: Client-Server
G s1 DB s2 DB s3 DB c1 c1 c1--s1 c1--s2 c1--s3 c2 c2 c2--s1 c2--s2 c2--s3 c3 c3 c3--s1 c3--s2 c3--s3
Drei-Schichten-Architektur
G s1 DB s2 DB s3 DB c1 c1 app app c1--app c2 c2 c2--app c3 c3 c3--app app--s1 app--s2 app--s3

Reverse Proxy

Webserver vor der eigentlichen Applikation
Kein absolut festgelegtes Aufgabenspektrum, typischerweise aber
  • Auslieferung von statischen Inhalten
  • Terminierung von HTTPS-Verbindungen
  • Routing von Anfragen an möglicherweise unterschiedliche Applikationen im Backend
Drei-Schichten-Architektur + Reverse Proxy
G s1 DB s2 DB s3 DB c1 c1 rproxy rproxy c1--rproxy c2 c2 c2--rproxy c3 c3 c3--rproxy app app rproxy--app app--s1 app--s2 app--s3

Load Balancing

Einführung eines speziellen Servers der die Anfragen auf mehrere Application-Server verteilt
  • Verarbeitung der Anfragen typischerweise aufwändig
  • Erlaubt die Auslastung von Mehrkernrechnern trotz Single-Thread-Frameworks
  • Technisch gesehen auch als reverse Proxy denkbar
Drei Schichten + Loadbalancer
G s1 DB s2 DB s3 DB app1 app app1--s1 app1--s2 app1--s3 app2 app app2--s1 app2--s2 app2--s3 app3 app app3--s1 app3--s2 app3--s3 c1 c1 loadbalancer loadbalancer c1--loadbalancer c2 c2 c2--loadbalancer c3 c3 c3--loadbalancer loadbalancer--app1 loadbalancer--app2 loadbalancer--app3

Content Delivery Network

Auslieferung von statischen Inhalten durch Dienstleister
  • Amazon Cloudfront, Cloudflare, Akamai
  • Applikationsserver nicht mit Auslieferung von “langweiligen” Inhalten beschäftigt
  • Weltweite Verteiltung erlaubt Serverstandorte näher am Kunden
Vorsicht bei Dienstleistern, die einen einfachen “Speedboost” versprechen
  • Das Content Delivery Network ist ein weiterer Cache, mit allen Vor- und Nachteilen
  • Einige Unternehmensnetzwerke (oder Länder) blocken die CDN-Endpunkte immer mal wieder
    • Im Falle von gehosteter Malware werden nicht selten ganze Domains blockiert
    • Zugriff in einigen Ländern rechtlich / politisch unterbunden
Drei Schichten + Content Delivery Network
G s1 DB s2 DB s3 DB c1 c1 app app c1--app cdn cdn c1--cdn c2 c2 c2--app c2--cdn c3 c3 c3--app c3--cdn app--s1 app--s2 app--s3 cdn--app

Web Application Firewall

Abfangen von gefährlichen Anfragen bevor sie die Applikation erreichen
Bekannte Exploits, SQL-Injection, Cross-Site-Scripting, …
Drei Schichten + Web Application Firewall
G s1 DB s2 DB s3 DB firewall firewall app app firewall--app c1 c1 c1--firewall c2 c2 c2--firewall c3 c3 c3--firewall app--s1 app--s2 app--s3

Alles auf einmal

Keine “universell richtige” Anordnung der Komponenten
Je nach Anwendungsfall z.B. Firewall nur für bestimmte Inhalte
N-Schichten
G s1 DB s2 DB s3 DB firewall firewall rproxy rproxy firewall--rproxy c1 c1 c1--firewall cdn cdn c1--cdn c2 c2 c2--firewall c2--cdn c3 c3 c3--firewall c3--cdn loadbalancer loadbalancer rproxy--loadbalancer app1 app1 loadbalancer--app1 app2 app2 loadbalancer--app2 app3 app3 loadbalancer--app3 app1--s1 app1--s2 app1--s3 app2--s1 app2--s2 app2--s3 app3--s1 app3--s2 app3--s3 cdn--app1 cdn--app2 cdn--app3

Single Page Applications am Beispiel von Angular

Im Folgenden: Allgemeine Problemstellungen, wie Sie in vielen JavaScript-Anwendungen vorkommen
Und deren Lösung speziell in Angular
“AngularJS”, “Angular”, “Angular 2” und “Angular 4”
  • AngularJS: Weit verbreitetes JavaScript-Framework von Google, hat fundamentale Schwächen in der Architektur
  • Angular 2: Ursprünglicher Name der auf AngularJS folgenden Version, Quelltexte sind absolut inkompatibel
  • Angular 4: Aktuelle Version des Angular Frameworks, Quelltexte sind bis auf sehr kleine Ausnahme absolut zu Angular 2 kompatibel
  • Angular: “Neuer” Name ohne “JS”-Suffix
Einzelne Aspekte von Angular lassen sich in einem “Plunkr” ausprobieren
  • Zweck: Kurze Demonstrationen im Rahmen dieser Vorlesung
  • Keinesfalls für mehr als sehr oberflächliche Experimente geeignet

Problem: Große Projekte

Bisher: Einsatz von clientseitigem JavaScript “im Kleinen” auf einzelnen Seiten
Sehr spezifischer Quelltext für spezielle Ausschnitte eines DOM-Baums
Auf oberster Ebene: Zwei grundsätzliche Reaktionen
  • Dynamisch typisierte Programmiersprachen nutzen eine Vielzahl ausführlicher Testmethodiken
  • Statische typisierte Programmiersprachen freuen sich über Fehlermeldungen vom Compiler
Die Angular-Lösung: Beide Reaktionen
Nutzt TypeScript als Programmiersprache und bietet sehr ausführliche Anleitungen zum Testen

Grundsätzliche Aufteilung in Module, Komponenten und Dienste

Module gruppieren logisch zusammenhängende Komponenten und definieren Abhängigkeiten
Angular-Anwendungen lassen sich bei Bedarf anhand von Modulen in unabhängige Einzelteile zerlegen
Komponenten sind speziell deklarierte Klassen, die als Datenquelle für Templates dienen
  • Zu jeder Komponente gehört genau ein Template
  • Jede Komponente kann über einen eindeutigen Bezeichner in Templates eingesetzt werden
Dienste können zum Austausch von Daten verwendet werden
Die vornehme Alternative zu globalen Variablen

Komponenten zur Anzeige von Daten

Wie bisher: Trennung von Daten und Ihrer Darstellung
Im Falle von Angular 2: Eigene Templatingsprache im Kontext der Komponentenklassen
Komponentenklasse werden nicht durch Vererbung, sondern durch “Dekoratoren” ausgezeichnet
  • Technische Implementierung an dieser Stelle nicht relevant, für Interessierte: Dokumentation zu Dekorator-Anweisungen
  • Mindestens Angabe eines Templates (als Pfad oder inline) und eines Namens
Definition normaler Eigenschaften und Methoden, die dann im Template zur Verfügung stehen
Allerdings unter Berücksichtigungen einiger Regeln für Seiteneffekte

Templating

Prinzip der automatischen Datenbindung: Template “aktualisiert sich selbst”
  • Automatische Reaktion auf Eingaben und weitere Ereignisse
  • Im Falle von Angular 2: Nutzt im Hintergrund Observables
Wesentlicher Unterschied zu Liquid: Angular arbeitet unmittelbar auf dem DOM, nicht mit Strings
  • Alle Möglichkeiten daher immer an DOM-Knoten gebunden
  • Template-Code wird zu JavaScript kompiliert
Zum Nachschlagen: Übersichtsseite der Angular-Dokumentation zur Templating-Syntax
Dokumentation im Allgemeinen ausgesprochen hochwertig

Operatoren zur Datenbindung

Einsetzen von Werten mit {{ expr }}
  • Oberflächlich ähnlich zu Liquid, erlaubt aber auch komplexere Ausdrücke (unter anderem Mathematik)
  • Technisch gesehen unterschiedliche Bedeutungen im Inhalt von Knoten und in Attributen
Binden von Attributwerten mit [name]="expr"
Alternative Schreibweise für name="{{ expr }}"
Reagieren auf Ereignisse mit (name)="expr"
Erlaubt ähnliche Notation wie JavaScript-Handler, z.B. in Form von onclick

Automatische Datenbindung mit ngModel

Zwei-Wege-Datenbindung mit [(ngModel)]="wert"
  • Das ngModel-Attribut bindet automatisch das value-Attribut und diverse change-Listener um Daten
  • Visualize a banana in a box to remember that the parentheses go inside the brackets.
Einbindung des Moduls für Formulare notwendig
“Importieren” kann sich im Kontext von Angular auf mehrere Dinge beziehen
  1. Importieren eines Typs aus einer anderen Datei
  2. Ergänzen eines Moduls in den import-Anweisungen eines anderen Moduls

Strukturelle Anweisungen in Templates

Strukturelle Anweisung ändern die Struktur des DOM
Werden in der Template-Syntax mit einem * als Präfix versehen
*ngIf="expr" zum ein- oder ausblenden von Inhalten
Sofern expr zu false auswertet, wird der Knoten samt aller Kinder automatisch entfernt
*ngFor="let <var> of <expr>;" zum Iterieren
  • Der Bezeichner var steht im Kontext der Schleife dann zur Verfügung
  • Weitere lokale Iterationsvariablen können mit as in den Kontext der Schleife geholt werden
    • index bezeichnet den Schleifenzähler
    • first und last sind für die erste bzw. letzte Iteration true
    • even und odd steht für gerade bzw. ungerade Schleifendurchläufe

Methodenaufrufe in Templates

Methoden der Komponenten können in Templates unmittelbar aufgerufen werden
Dabei stehen auch Lauf- oder andere Variablen zur Verfügung
Verweis auf mit #name benannte DOM-Elemente
Zugriff erfolgt nicht über das ID-Attribut!

Weitergabe von Daten mit @Input()

Grundidee: Anzeige einzelner Datensätze an spezielle Komponenten auslagern
Übergabe von Parametern funktioniert für Komponenten exakt so wie für HTML-Elemente
Vorsicht: Nur mit @Input() versehene Eigenschaften von Komponenten können im HTML gebunden werden
Wenn die @Input()-Attributierung fehlt, kann die Anzeige bei Veränderungen nicht aktualisiert werden

Wichtig: Keine Seiteneffekte

Auswertung von Templates darf keine Änderungen an den Daten vornehmen
Trivial für Variablen, gilt aber auch für Methoden und Eigenschaften mit get
Bei Seiteneffekten: Jedes Rendern des Templates würde einen einen neuen Render-Prozess auslösen
  • Einer der wesentlichen Gründe für die komplette Neuentwicklung
  • Im Entwicklungsmodus: Hinweis von der Angular Runtime bei aufgetretenen Seiteneffekten

Das Modulsystem von Angular

Zweck: Schlankere Anwendungen zur Laufzeit
  • Nicht benötigte Module müssen nicht übertragen werden
  • Laden von größeren Modulen erst bei Bedarf
Vorgehen: Jedes Modul definiert die von ihm bereitgestellten und benötigten Abhängigkeiten
  • Benötigt werden andere Module
  • Angeboten werden Komponenten oder Dienste
Sonderfall: Ein Modul bildet die Wurzel für die gesamte Anwendung
  • Heißt per Konvention AppModule und landet in der Datei app.module.ts
  • Definiert mindestens eine bootstrap-Komponente die nach dem Laden angezeigt wird
Parameter zur Erstellung von Modulen sind grundsätzlich Typen (konkreter: meistens Klassen)
  • declarations: Jede im Modul verwendete Komponente
  • exports: Außerhalb dieses Moduls verfügbare Komponenten
  • providers: Jeder von diesem Modul angebotene Dienst

Dependency Injection

Grundidee: Abhängigkeiten von anderen Klassen im Konstruktor deutlich machen
Keinen Zugriff auf globale Objekte, Singletons, …
Ziel: Lose Kopplung der beteiligten Klassen
  • Dienste zur Bereitstellung von Daten
  • Komponenten als Konsumenten dieser Daten
  • Kommunikation von Änderungen (unter anderem) über Observables

Konsumieren

Vorgehen: Keine Verwendung des new-Operators im normalen Code
Stattdessen Verwendung spezieller Konstruktionsmethoden oder ganzer Frameworks
Im Falle von Angular: Bereitzustellende Instanz eindeutig über den Typ identifizierbar
Zusätzlich möglich: Angabe von künstlichen Bezeichnern zur Identifizierung
Übergebenes Objekt ist nicht zwingend identisch, aber kompatibel zu gefordertem Typ
Abweichende Typen für zum Beispiel …
  • “Defekte” Provider zur Simulation von Netzwerkausfällen in Testfällen
  • “Intelligentere” Provider mit Caching-Verhalten
  • Unterschiedliche Logging Provider
  • spa/angular-di-intro.ts
    @Component({
      selector: 'profile-show',
      templateUrl : 'src/profile-show.html'
    })
    export class ProfileShowComponent {
        private _up : UserProvider;
        private _cp : CreditProvider;
        private _log : LogService;
      
        constructor(up : UserProvider,
                    cp : CreditProvider,
                    log : LogService) {
            this._up = up;
            this._cp = cp;
            this._log = log
        }
    }
    

Bereitstellung

Vorgehen: Bereitstellung eines “Injizierbaren” Typs
Injizierbare Klassen werden mit @Injectable() markiert und in die providers-Liste eines Moduls (oder seltener: einer Komponente) eingetragen
Standardverhalten: Jeder Dienst wird exakt einmalig erzeugt
Austausch von Daten zwischen Konsumenten also über Dienste möglich
  • spa/angular-di-logger.ts
    abstract class Logger {
        abstract info(...args: any[]);
    }
    
    @Inject()
    class ConsoleLogger extends Logger {
    
        info(...args: any[]) {
            console.log.apply(console, args);
        }
    }
    
    @Inject()
    class NetworkLogger extends Logger {
        info(...args: any[]) {
            // Send via XHR
        }
    }
    
  • spa/angular-di-logger-provider.ts
    // In der Moduldefinition
    
    // Mögliches `providers` Array #1
    [{ provide: Logger, useClass: ConsoleLogger }]
    
    // Mögliches `providers` Array #2
    [{ provide: Logger, useClass: NetworkLogger }]
    
  • spa/angular-di-intro.ts
    @Component({
      selector: 'profile-show',
      templateUrl : 'src/profile-show.html'
    })
    export class ProfileShowComponent {
        private _up : UserProvider;
        private _cp : CreditProvider;
        private _log : LogService;
      
        constructor(up : UserProvider,
                    cp : CreditProvider,
                    log : LogService) {
            this._up = up;
            this._cp = cp;
            this._log = log
        }
    }
    

Vollständiges Beispiel zu Dependency Injection

In Angular: Baum verfügbarer Dienste, welche automatisch in Konstruktoren verfügbar sind
Empfehlung des Angular-Teams: Dienste immer an der Wurzelklasse registrieren

Routing

Navigation wie bisher durch Angabe einer URL
Als Reaktion allerdings nicht notwendigerweise eine Anfrage an den Server
Grundsätzliches Vorgehen: Assoziation von Routen mit Komponenten
  • Zielkomponente wird in einen als <router-outlet> ausgezeichneten Bereich der Seite geladen
  • Umgebender Bereich bleibt unverändert und kann für z.B. Header, Footer und Navigation genutzt werden
Assoziation findet im Rahmen der Moduldefinition statt
[ { path: 'entities', component: EntityListComponent }, { path: 'about', component: AboutComponent } ]

Konkretes Routing-Beispiel

Notwendig: Import des Router-Moduls, Anwendungen ohne Routing ebenfalls denkbar
  • Navigation innerhalb der Anwendung möglich, aber dann stets identischer Einstiegspunkt für alle Besucher
  • Damit auch keine Weitergabe von URLs auf spezifische Inhalte möglich
Verlinkung der Inhalte über Attribut routerLink (Angular-Doku)
  • Fängt “typische” Reaktion des Browsers ab und setzt automatisch das href-Attribut
  • Angabe eines Strings oder eine Liste von Segmenten
  • Verwendung von Variablen oder Daten aus der Komponente möglich
Vorsicht: Unterschiedliche Aufrufe für den Import des Routing Moduls am Wurzel-Modul
Variante für Wurzel-Modul (forRoot()) übernimmt zusätzliche Initialisierungsschritte (z.B. die Bereitstellung von router-outlet)

Serverseitige Behandlung

Vorsicht: Routing existiert nur innerhalb der clientseitigen Anwendung
Server muss unbekannte Routen ggfs. auf die index.html mappen
  • spa/angular-server-catchall.js
    
    // Zuerst: Zwei API-Endpunkte für die Anwendung
    app.get('/api/hello', function (req, res) {
      res.send('Hello World!')
    });
    
    app.post('/api/hello', function (req, res) {
      res.send(200, 'Stored');
    });
    
    // Dann: Ausliefern von statischen Dateien
    app.use('/', express.static('.'));
    
    // Neu: Unbekannte Routen auf die index.html mappen
    // Es ist jetzt Aufgabe der SPA eine Fehlermeldung zu zeigen
    // wenn die Route keinen Sinn ergibt.
    app.get('*', function(req, res) {
      // `__dirname` steht für den Ordner der ausführenden Datei
      res.sendFile(path.join(__dirname, './index.html'));
    });
    
    app.listen(port, function () {
      console.log(`Express-Server listening on port ${port}!`)
    });
    
  • spa/index.html | Validieren
    <html>
      <head>
        <title>SPA</title>
        <link rel="stylesheet" type="text/css" href="index.css"/>
      </head>
      <body>
        <h1>Wenn ich groß bin, werde ich eine Web-Anwendung</h1>
      </body>
    </html>
    
  • spa/index.css
    body {
        background-color: fuchsia;
    }
    

Routing mit Parametern

Grundlegende Syntax für Routen ähnlich wie bei express.js: Parameter werden mit :param notiert
Abbildung erfolgt auf eine Komponente
Grundgedanke: Parameter in der URL sind auch nur (veränderliche) Variablen
Reaktion auf Veränderungen im Kern also über Observable.subscribe
Konkretes Abonnement: paramMap der aktivierten Route
Wird aktualisiert wenn der Benutzer navigiert und dabei ausschließlich die Parameter in der URL verändert

Interaktion mit Servern

Angular verfügt über einen HTTP-Dienst zur Kommunikation mit dem Server
Interne Implementierung wieder über XmlHTTPRequest
Grundsätzliche Syntax: HTTP-Verb mit URL und ggfs. Rumpfdaten (Angular Doku)
Http.get(url: string, options?: RequestOptionsArgs): Observable<Response>
Antwort erfolgt über ein in ein Observable verpacktes Response-Objekt (Angular Doku)
json-Methode auf dem Response-Objekt zum einfachen Zugriff auf die Daten
Hilfreich: Verwendung des Elvis-Operators ?. zum Zugriff auf möglicherweise undefinierte Werte (Angular Doku)
  • Bricht weitere Verarbeitung ohne Fehler ab
  • Funktioniert nur in der Angular Templating-Sprache

Erste Möglichkeit: Daten nach Abruf einer Variablen zuweisen

Vorgehen: Bereitstellen einer refresh-Methode, welche eine Eigenschaft zuweist
Aufruf der refresh-Methode natürlich auch mehrfach möglich
subscribe-Aufruf findet innerhalb von refresh statt
  • Auspacken der Daten aus dem Observable
  • Daten mit z.B. map in die richtige Form bringen
Unschön: refresh-Funktion hat void-Signatur
Aus dem Aufruf ist daher nicht erkennbar, wann die Daten bereitstehen
  • spa/angular-http-list-explicit.ts
    @Injectable()
    class EntityProvider {
      private _entities : ListedEntity[] = [];
      private _http : Http;
    
      constructor(http: Http) {
        this._http = http;
        console.log("Started Entity Provider", this._http);
        this.refreshEntities();
      }
    
      get entities() {
        return (this._entities);
      }
    
      private refreshEntities() {
        this._http
          .get(`https://webanwendungen.fh-wedel.de/interactive/api/entity`)
          .do(res => console.log(`1 - List Response:`, res))
          .map(res => res.json())
          .do(res => console.log(`2 - List response as JSON:`, res))
          .map(res => res.map(e => new ListedEntity(e.id, e.name)))
          .do(res => console.log(`3 - List response as ListedEntity[]:`, res))
          .subscribe(res => this._entities = res)
      }
    }
    

Zweite Möglichkeit: Impliziter Abruf eines Datums mit der async-Pipe

Schön: refresh-Funktion (oder sinniger get-Funktion) hat Observable-Rückgabewert
Aufrufer kann selber feststellen, wann die Daten angekommen sind
Im Template: async-Pipe “entpackt” den Wert aus dem Observable
Für nicht-HTTP-Observables sinnvoll
  • spa/angular-http-async.ts
    @Injectable()
    class EntityProvider {
      private _http : Http;
    
      constructor(http: Http) {
        this._http = http;
        console.log("Started Entity Provider", this._http);
      }
    
      get entities() : Observable<ListedEntity[]> {
        return (
          this._http
            .get(`https://webanwendungen.fh-wedel.de/interactive/api/entity`)
            .do(res => console.log(`1 - List Response:`, res))
            .map(res => res.json())
            .do(res => console.log(`2 - List response as JSON:`, res))
            .map(res => res.map(e => new ListedEntity(e.id, e.name)))
            .do(res => console.log(`3 - List response as ListedEntity[]:`, res))
        );
      }
    }
    

Vorsicht: HTTP-Dienst hat keinen Cache

Observable<Response> erzeugt eine HTTP-Anfrage wenn subscribe aufgerufen wird
Kein eingebauter Cache: Jeder Aufruf von subscribe löst eine neue Anfrage aus
  • spa/angular-http-async-mult.ts
    @Injectable()
    class EntityProvider {
      private _http : Http;
    
      constructor(http: Http) {
        this._http = http;
        console.log("Started Entity Provider", this._http);
      }
    
      entityById(id : number) : Observable<Entity> {
        const host = `https://webanwendungen.fh-wedel.de`;
        return (
          this._http
            .get(`${host}/interactive/api/entity/${0}`)
            .do(res => console.log(`1 - Detail Response:`, res))
            .map(res => res.json())
            .do(res => console.log(`2 - Detail response as JSON:`, res))
            .map(res => new Entity(res.id, res.name, res.author))
            .do(res => console.log(`3 - Detail response as Entity:`, res))
          );
      }
    }
    

Dritte Möglichkeit: Daten nach Abruf einem Observable zuweisen

Schön: Zugriffsfunktion hat Observable-Rückgabewert
Aufrufer kann selber feststellen, wann die Daten angekommen sind

Vollständiges Beispiel zur serverseitigen Kommunikation mit Angular