Single Page Application
Dieses Kapitel ist auf dem Stand vom Sommersemester 2017 und ist nur auf expliziten Wunsch einzelner Studierender erneut öffentlich. Es wurde im Sommersemester 2018 nicht gelesen und befindet sich (inklusive aller Beispiele) auf dem Stand von Angular 4 . Leider sind auch einige der Beispiele deswegen nicht lauffähig , das wird sich bedauerlicherweise wohl auch nicht mehr ändern, da ich die Vorlesung letztmalig gehalten habe.
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
Nimmt mutierende Anfragen entgegen oder liefert Antworten auf bestimmt Fragestellungen, häufig im Zusammenspiel mit einer Datenbank
Verwendet häufig serverseitige Bibliotheken
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: Wikipedia, CC-BY )
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
Importieren eines Typs aus einer anderen Datei
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!
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
@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
// In der Moduldefinition
// Mögliches `providers` Array #1
[{ provide: Logger, useClass: ConsoleLogger }]
// Mögliches `providers` Array #2
[{ provide: Logger, useClass: NetworkLogger }]
@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 URL
s 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
// 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}!`)
});
<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>
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
@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
@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
Für Observables von HTTP
-Anfragen nicht sinnvoll. Im Rahmen von Aufgabe 4 lieber auf die 1. Möglichkeit zurückgreifen.
@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
Entfällt aus Zeitgründen :( Neuer Wert sollte an die `next` Methode eines `Subject` übergeben werden.
Vollständiges Beispiel zur serverseitigen Kommunikation mit Angular