Rezepte für den Datenaustausch
In der Regel möchten wir bei Web-Anwendungen in der Lage sein, mit einem Server Daten auszutauschen. Wir möchten zur Laufzeit sowohl Daten nachladen als auch Daten, die uns der Nutzer gibt, an den Server schicken. In diesem Kapitel werden wir den Http-Service von Angular kennenlernen und sehen, wie wir ihn nutzen können, um mit einem Server zu kommunizieren.
Daten vom Server mit GET holen
Problem
Ich möchte zur Laufzeit Daten im JSON-Format von einem Server holen.
Zutaten
- Ein Service, um die Daten zu holen
- Das Http-Modul von Angular
- Der Http-Service von Angular
- Die map-Methode für Observables (Ist Teil von RxJS)
- Auf Nutzer-Input reagieren (Wir holen Daten nach einem Klick auf einen Button)
- Liste von Daten anzeigen
- Anpassungen an der app.module.ts-Datei
- Anpassungen an der package.json-Datei
Lösung
In dieser Lösung werden wir sehen, wie wir JSON-Daten von einem Server holen können. Die Fehlerbehandlung lassen wir außen vor, damit wir uns fürs Erste auf die GET-Anfrage konzentrieren können. Über Fehler bei Server-Anfragen reden wir im Rezept “Server-Anfragen und Fehlerbehandlung”.
Wir gehen hier davon aus, dass wir einen Server haben, der auf 127.0.0.1:3000 hört. Wenn eine GET-Anfrage an /data geschickt wird, antwortet der Server mit Status 200 und Daten im JSON-Format. Wir nutzen http://127.0.0.1:3000/data als URL für die Anfrage. Die Daten sehen wie folgt aus:
1 {
2 "data": [{"id": 1, "name": "a"}, {"id": 2, "name": "b"}]
3 }
Jetzt implementieren wir einen Service, um die GET-Anfrage zu schicken.
1 import { Injectable } from '@angular/core';
2 import { Http } from '@angular/http';
3 import 'rxjs/add/operator/map';
4
5 @Injectable()
6 export class DataService {
7 http: Http;
8 url: string;
9 constructor(http: Http) {
10 this.http = http;
11 this.url = 'http://127.0.0.1:3000/data';
12 }
13
14 getData() {
15 const observable = this.http.get(this.url);
16 const anotherObservable = observable.map((response) => response.json().data);
17 return anotherObservable;
18 }
19 }
Erklärung:
- Zeile 2: Hier importieren wir den Http-Service von Angular
- Zeile 3: Durch diesen Import erweitern wir die Instanzen der Observable-Klasse (siehe Zeile 16) um eine Methode namens “map”
- Zeile 5: Im Gegensatz zum Service im Rezept “Ein Service definieren” ist der Injectable-Decorator hier nicht optional, da unser Service eine Abhängigkeit besitzt: den Http-Service (siehe Zeile 9)
- Zeile 9: Hier definieren wir den Http-Service als Abhängigkeit unseres Services
- Zeile 15: Hier rufen wir die get-Methode des Http-Services auf. Als Parameter erhält diese Methode eine URL. Der Rückgabewert ist ein Observable (Teil von RxJS)
- Zeile 16: Wir nutzen die map-Methode, um die Antwort des Servers zu transformieren. Der response-Parameter ist eine Instanz der Response-Klasse und besitzt eine json-Methode (.json()), die die Daten des Servers in ein Objekt transformiert
Jetzt müssen wir noch unsere Komponente aus “Ein Service definieren” anpassen, so dass diese mit dem Http-Service und Observables arbeiten kann. Wir werden die Daten nach einem Klick auf den “Get Data”-Button holen und diese in einer Liste anzeigen.
1 import { Component } from '@angular/core';
2
3 import { DataService } from './data.service';
4
5 interface Data {
6 id: number;
7 name: string;
8 }
9
10 @Component({
11 selector: 'app-root',
12 template: `
13 <button (click)="getData()">Get Data</button>
14 <ul>
15 <li *ngFor="let d of data">ID: {{d.id}} Name: {{d.name}}</li>
16 </ul>
17 `
18 })
19 export class AppComponent {
20 dataService: DataService;
21 data: Array<Data>;
22
23 constructor(dataService: DataService) {
24 this.dataService = dataService;
25 this.data = [];
26 }
27
28 getData() {
29 this.dataService.getData()
30 .subscribe((data) => {
31 this.data = data;
32 });
33 }
34 }
Erklärung:
- Zeile 21: Die data-Eigenschaft wird benutzt, um die Daten in der Liste anzuzeigen. Sie ist vom Typ “Data” (Siehe Zeilen 5-8)
- Zeilen 28-33: Methode, die aufgerufen wird, wenn der Nutzer auf den “Get Data”-Button klickt
- Zeile 30: Die getData-Methode des Services gibt ein Observable zurück. Jedes Observable hat eine subscribe-Methode die wir nutzen können, um auf Änderungen zu reagieren, indem wir der Methode eine Callback-Funktion übergeben
Da sich der Http-Service in einem eigenen Angular-Modul befindet, müssen wir dieses Modul in unser “AppModule” importieren.
1 import { NgModule } from '@angular/core';
2 import { BrowserModule } from '@angular/platform-browser';
3 import { HttpModule } from '@angular/http';
4
5 import { AppComponent } from './app.component';
6 import { DataService } from './data.service';
7
8 @NgModule({
9 imports: [ BrowserModule, HttpModule ],
10 declarations: [ AppComponent ],
11 bootstrap: [ AppComponent ]
12 providers: [ DataService ]
13 })
14 export class AppModule { }
Erklärung:
- Zeile 8: Hier importieren wir das “HttpModule” in unser Modul. Jetzt können wir alle Services, die dieses Modul definiert in unserem Code nutzen
Da sich das “HttpModule” in einem eigenen npm-Paket befindet, müssen wir dieses auch in der package.json-Datei deklarieren.
1 {
2 ...
3 "dependencies": {
4 ...
5 "@angular/http": "2.1.2"
6 ...
7 }
8 ...
9 }
Wenn eine Angular-Anwendung mit angular-cli initialisiert wird, wird das “HttpModule” automatisch von angular-cli importiert und das entsprechende npm-Paket in der package.json-Datei deklariert.
Diskussion
Wir hätten den Http-Service auch direkt in unserer Komponenten nutzen können. Wir haben uns stattdessen für die Nutzung eines weiteren Services entschieden. Der Grund dafür ist, dass wir die Logik für den Aufruf nicht in unserer Komponenten haben wollen. Die Komponente interessiert sich nicht dafür, wie wir die Daten bekommen. Diese benötigt nur ein Array mit Daten. Woher dieses Array stammt, ist der Komponente egal. Mit dem gesonderten Service ist es einfacher z. B. die URL zu ändern, ohne dass wir die Komponente anpassen müssen. Wir passen nur den Service an und alle Komponenten, die diesen Service benutzen, werden weiterhin funktionieren. Es ist allgemein ein “Best Practice” unsere Komponenten schlank zu halten und Logik wie z. B. “Wie hole ich Daten?” einem Service zu überlassen.
Code
Code für den Server: server.js. Der Server funktioniert mit Node.js.
Weitere Ressourcen
- Offizielle Dokumentation für das response-Objekt auf der Angular 2 Webseite
- Offizielle Http-Service Dokumentation auf der Angular 2 Webseite
- Die RxJS-Dokumentation bietet weitere Informationen über Observables und Observer
Daten mit POST an den Server schicken
Problem
Ich möchte mittels POST-Anfrage Daten an einen Server schicken.
Zutaten
Lösung
Wir werden hier die Lösung aus dem Rezept “Daten vom Server mit GET holen” erweitern, so dass wir auch Daten an den Server schicken können. Auch hier lassen wir die Fehlerbehandlung außen vor. Siehe dazu “Server-Anfragen und Fehlerbehandlung”.
Wir gehen davon aus, dass wir einen Server haben, der auf 127.0.0.1:3000 hört. Wenn eine POST-Anfrage an /data geschickt wird, antwortet der Server mit Status 200 und Daten im JSON-Format. Bei der Anfrage erwartet der Server ein Objekt im JSON-Format mit einer name-Eigenschaft. Für die Antwort wird dieses Objekt um eine id-Eigenschaft erweitert. Wir nutzten http://127.0.0.1:3000/data als URL für die Anfrage.
1 {
2 "name": "New Name"
3 }
1 {
2 "data": {
3 "id": 3,
4 "name": "New Name"
5 }
6 }
Unser DataService wird um eine neue Methode und einen Import erweitert. Der Rest bleibt zum Rezept “Daten vom Server mit GET holen” gleich.
1 import { Injectable } from '@angular/core';
2 import { Http } from '@angular/http';
3 import 'rxjs/add/operator/map';
4
5 @Injectable()
6 export class DataService {
7
8 constructor(http: Http) {...}
9
10 getData() {...}
11
12 sendData(name) {
13 const data = { name: name };
14
15 const observable = this.http.post(this.url, data);
16 const anotherObservable = observable.map((response) => response.json().data);
17 return anotherObservable;
18 }
19 }
Erklärung:
- Zeilen 12-18: Methode, die wir aufrufen, um Daten an den Server zu schicken
- Zeile 13: Die Daten, die wir zum Server schicken wollen
- Zeile 15: Aufruf der post-Methode mit den Daten als zweitem Parameter
Unsere Komponente wird auch um eine sendData-Methode erweitert.
1 ...
2
3 @Component({
4 selector: 'app-root',
5 template: `
6 <button (click)="getData()">Get Data</button>
7 <button (click)="sendData()">Send Data</button>
8 <ul>
9 <li *ngFor="let d of data">ID: {{d.id}} Name: {{d.name}}</li>
10 </ul>
11 `
12 })
13 export class AppComponent {
14
15 ...
16
17 constructor(dataService: DataService) {...}
18
19 getData() {...}
20
21 sendData() {
22 const name = 'New Name';
23 this.dataService.sendData(name)
24 .subscribe((data) => {
25 this.data.push(data);
26 });
27 }
28 }
Erklärung:
- Zeile 7: Neuer Button. Bei Klick wird die sendData-Methode aufgerufen
- Zeilen 21-27: Methode, die aufgerufen wird, um Daten zu schicken
- Zeile 22: Die Daten, die wir schicken wollen
- Zeile 23: Aufruf der sendData-Methode des DataService. Diese Methode gibt ein Observable zurück.
- Zeile 24: Die Callback-Funktion (Observer) der subscribe-Methode wird aufgerufen, wenn der Server uns eine Antwort geschickt hat. Die data-Variable ist ein Objekt mit den Eigenschaften “name” und “id”
- Zeile 25: Das neue Objekt wird der Liste mit den Daten hinzugefügt
Diskussion
Wir haben bis jetzt ein Detail über die Observables, die die Methoden der Http-Klasse zurückgeben, verschwiegen. Die subscribe-Methode ist nicht nur eine Möglichkeit, mittels einer Callback-Funktion auf Änderungen zu reagieren. Ohne den Aufruf der subscribe-Methode (mit oder ohne Callback) würde Angular gar keine Server-Anfrage schicken. Der Grund dafür ist, dass die Http-Methoden sogenannte “Cold Observables” zurückgeben. Cold Observables führen erst dann die gewünschte Operation, wenn jemand die subscribe-Methode des Observable aufruft. In unserer Lösung ist die Operation die POST-Anfrage und unser “jemand” die sendData-Methode der Komponente. Siehe auch Cold vs. Hot Observables.
Code
Code für den Server: server.js. Der Server funktioniert mit Node.js.
Server-Anfragen und Fehlerbehandlung
Problem
Ich möchte dem Nutzer eine sinnvolle Fehlermeldung anzeigen, wenn bei einer Server-Anfrage etwas schief läuft.
Zutaten
Lösung
Wir werden den Code aus dem Rezept “Daten vom Server mit GET holen” anpassen. Fehler bei POST- und weiteren Server-Anfragen können wir analog behandeln.
Wir gehen hier davon aus, dass wir einen Server haben der auf 127.0.0.1:3000 hört. Wenn eine Anfrage nach /error geschickt wird, antwortet der Server mit Status 500 und Daten im JSON-Format. Wir nutzen http://127.0.0.1:3000/error als URL für die Anfrage.
1 {
2 "error": "Invalid Url"
3 }
1 ...
2
3 import 'rxjs/add/operator/catch';
4 import { Observable } from 'rxjs/Observable';
5 import 'rxjs/add/observable/throw';
6
7 @Injectable()
8 export class DataService {
9 ...
10
11 constructor(http: Http) {
12 this.http = http;
13 this.url = 'http://127.0.0.1:3000/data';
14 }
15
16 getData() {
17 const observable = this.http.get(this.url)
18 .map((response) => response.json().data);
19 const anotherObservable = observable.catch((response) => {
20 return this.handleResponseError(response);
21 });
22 return anotherObservable;
23 }
24
25 handleResponseError(response) {
26 let errorString = '';
27 if (response.status === 500) {
28 errorString = `Server error: ${response.json().error}`;
29 } else {
30 errorString = 'Some error occurred';
31 }
32 return Observable.throw(errorString);
33 }
34 }
Erklärung:
- Zeile 3: Durch diesen Import erweitern wir die Instanzen der Observable-Klasse (siehe Zeile 18) um eine Methode namens “catch”
- Zeile 4: Hier importieren wir die Observable-Klasse von RxJS
- Zeile 5: Durch diesen Import erweitern wir die Observable-Klasse (siehe Zeile 31) um eine statische Methode namens “throw”
- Zeile 13: Die URL, um einen Server-Fehler zu erzwingen
- Zeilen 19-21: Hier wird die catch-Methode benutzt, um Fehler beim Server-Aufruf zu behandeln
- Zeile 20: Wenn ein Fehler auftritt, wird die handleResponseError-Methode aufgerufen
- Zeilen 25-33: Methode, um Server-Fehler zu behandeln
- Zeile 32: Wir geben eine Observable-Instanz mit Fehler zurück, so dass der zweite Parameter der subscribe-Methode aufgerufen wird (siehe Zeile 24 im Ausschnitt aus der app.component.ts-Datei)
Den eigentlichen Server-Fehler haben wir schon im Service mit Hilfe der catch-Methode behandelt. Da wir zusätzlich dem Nutzer eine sinnvolle Fehlermeldung anzeigen möchten, müssen wir den Fehler auch in der Komponente behandeln.
1 ...
2
3 @Component({
4 selector: 'app-root',
5 template: `
6 <button (click)="getData()">Get Data</button>
7 <p>
8 Error: {{errorText}}
9 </p>
10 <ul>
11 <li *ngFor="let d of data">ID: {{d.id}} Name: {{d.name}}</li>
12 </ul>
13 `
14 })
15 export class AppComponent {
16 ...
17
18 errorText: string;
19
20 constructor(dataService: DataService) {... }
21
22 getData() {
23 this.dataService.getData()
24 .subscribe((data) => {
25 this.data = data;
26 }, (errorText) => {
27 this.errorText = errorText;
28 });
29 }
30 }
Erklärung:
- Zeile 8: Fehlermeldung in der View anzeigen
- Zeilen 26-28: Fehlerbehandlungsfunktion als zweiter Parameter der subscribe-Methode
- Zeile 27: Fehlertext von Zeile 31 des Services als Wert der errorText-Eigenschaft setzen
Diskussion
Die catch-Methode von Instanzen der Observable-Klasse ist vergleichbar mit der catch-Methode einer Promise-Kette oder dem catch-Block einer try/catch-Anweisung. Jeder Fehler in der Kette der Observables, der vor der catch-Methode auftritt, kann in der catch-Methode behandelt werden. Ähnlich wie bei einem try/catch und bei Promises wird bei einen Fehler die Ausführungs-Kette “unterbrochen” und wir springen direkt zu der catch-Methode. Bei der Fehlerbehandlung in der catch-Methode können wir eine Instanz eines Observable mit Fehler zurückgeben, um erneut einen Fehler zu erzeugen. Das ist ähnlich zu einer throw-Anweisung in einem catch-Block einer try/catch-Anweisung.
Wie wir in diesem Rezept gesehen haben, kann die subscribe-Methode nicht nur eine Callback-Funktion als Parameter erhalten, sondern zwei (genauer gesagt sind es drei aber der dritte Parameter ist für uns vorerst nicht relevant). Wir wissen schon, dass die erste Callback-Funktion aufgerufen wird, wenn die Server-Anfrage erfolgreich ist. Die zweite Callback-Funktion wird im Falle eines Fehlers aufgerufen. Diese Callback-Funktion ist unsere zweite Möglichkeit, einen Fehler in einer Observables-Kette zu behandeln. Wir haben also den Fehler an zwei verschiedenen Orten behandelt: Einmal in unserem Service mittels catch-Methode und einmal in unserer Komponente mit Hilfe des zweiten Parameters der subscribe-Methode. Prinzipiell wäre es möglich, den Fehler entweder im Service oder in der Komponente zu behandeln. Der zweite Parameter der subscribe-Methode ist optional und optional ist auch die Nutzung der catch-Methode. Der Grund, weshalb wir den Fehler an zwei Stellen behandeln, ist ganz einfach. Wir wollen nicht, dass unsere Komponente wissen muss, wie die Server-Antwort im Falle eines Fehlers aussieht, genauso, wie wir nicht wollten, dass die Komponente weiß, was mit einer erfolgreichen Server-Antwort zu tun ist, bevor Daten angezeigt werden können. Der Komponente reicht es, Daten bzw. Fehlermeldungen zu erhalten, die direkt angezeigt werden können.
Code
Code für den Server: server.js. Der Server funktioniert mit Node.js.
Server-Anfrage mit Query-Parametern
Problem
Ich möchte bei der Anfrage Query-Parameter an den Server schicken.
Zutaten
- Daten vom Server mit GET holen
- Die URLSearchParams-Klasse von Angular
- Die RequestOptions-Klasse von Angular
Lösung
Wir konzentrieren uns in der Lösung auf GET-Anfragen, da diese am Häufigsten mit Query-Parametern benutzen werden. Wir können aber auch z. B. bei POST-Anfragen Query-Parameter mitschicken.
1 import { Injectable } from '@angular/core';
2 import {
3 Http,
4 RequestOptions,
5 URLSearchParams
6 } from '@angular/http';
7 import 'rxjs/add/operator/map';
8
9 @Injectable()
10 export class DataService {
11
12 ...
13
14 getData() {
15 const limit = 1;
16
17 const params = new URLSearchParams();
18 params.set('limit', String(limit));
19
20 const requestOptions = new RequestOptions({search: params});
21
22 const observable = this.http.get(this.url, requestOptions);
23 const anotherObservable = observable.map((response) => response.json().data);
24 return anotherObservable;
25 }
26 }
Erklärung:
- Zeile 17: Erzeugen einer Instanz der URLSearchParams-Klasse
- Zeile 18: Query-Parameter “limit” mit Wert
'1'(der zweite Parameter der set-Methode muss ein String sein) definieren - Zeile 20: Erzeugen einer Instanz der RequestOptions-Klasse. Wir setzen unsere “params” als Wert der search-Eigenschaft. Die search-Eigenschaft definiert die Query-Parameter der Anfrage
- Zeile 22: Aufruf der get-Methode mit einer URL und Optionen für die Anfrage
Diskussion
Wir können die Query-Parameter auch mittels String-Konkatenierung definieren, indem wir selbst einen Query-String zusammensetzen und diesen mit der URL konkatenieren. Für ein bis zwei Parameter können wir dies auch tun, aber für viele Parameter ist diese Lösung nicht wirklich geeignet. Die Nutzung der URLSearchParams-Klasse hat in diesem Fall zwei Vorteile. Zum Einen wird der Code lesbarer, wenn wir pro Parameter eine Zeile Code haben. Zum Anderen kümmert sich Angular um das richtige Format für den String, der später als Teil der URL mitgeschickt wird.
Code
Code für den Server: server.js
Weitere Ressourcen
- Offizielle URLSearchParams-Dokumentation auf der Angular 2 Webseite
- Offizielle RequestOptions-Dokumentation auf der Angular 2 Webseite
Server-Anfrage abbrechen (cancel)
Problem
Ich möchte dem Nutzer die Möglichkeit anbieten, eine Server-Anfrage abzubrechen, wenn diese zu lange dauert.
Zutaten
- Daten vom Server mit GET holen
- Neue URL, um eine Anfrage zu simulieren, die drei Sekunden braucht
- Änderungen an der Komponente aus “Daten vom Server mit GET holen”
- Die Subscription-Klasse von RxJS (wird nur für die Typdefinition gebraucht)
Lösung
In unserem Service (data.service.ts) haben wir nur eine Änderung durchgeführt. Und zwar haben wir die url-Eigenschaft angepasst. Diese besitzt jetzt den Wert 'http://127.0.0.1:3000/longrequest'.
1 ...
2
3 import { Subscription } from 'rxjs/Subscription';
4
5 ...
6
7 @Component({
8 selector: 'app-root',
9 template: `
10 <button (click)="getData()">Get Data</button>
11 <button (click)="cancelRequest()">Cancel</button>
12 <ul>
13 <li *ngFor="let d of data">ID: {{d.id}} Name: {{d.name}}</li>
14 </ul>
15 `
16 })
17 export class AppComponent {
18 ...
19
20 subscription: Subscription;
21
22 constructor(dataService: DataService) {...}
23
24 getData() {
25 this.subscription = this.dataService.getData()
26 .subscribe((data) => {
27 this.data = data;
28 });
29 }
30
31 cancelRequest() {
32 if (this.subscription) {
33 this.subscription.unsubscribe();
34 }
35 }
36 }
Erklärung:
- Zeile 3: Hier importieren wir die Subscription-Klasse von RxJS, die wir in Zeile 17 als Typ nutzen
- Zeile 25: Hier speichern wir den Rückgabewert der subscribe-Methode in die subscription-Eigenschaft der Komponenteninstanz
- Zeilen 31-35: Methode, die aufgerufen wird, wenn der Nutzer den cancel-Button klickt
Diskussion
Die subscribe-Methode der Instanzen der Observables-Klasse gibt eine Instanz der Subscription-Klasse zurück. Wir können auf dieser Instanz die unsubscribe-Methode aufrufen, um die dazugehörigen Observables wegzuwerfen. Nach dem Aufruf der unsubscribe-Methode werden die Observables gelöscht und deren Callback-Funktionen nicht mehr aufgerufen. Auch z. B. die Callback-Funktion der map-Methode (diese wird in der data.service.ts benutzt) wird nicht mehr aufgerufen. Also erzeugen die Observables keine neuen Werte mehr und der Fluss (stream) wird unterbrochen. Bei Server-Anfragen wird auch die abort-Methode der XMLHttpRequest-Instanz aufgerufen und die Anfrage wird abgebrochen.
Code
Code für den Server: server.js. Der Server funktioniert mit Node.js.
Weitere Ressourcen
- Die RxJS-Dokumentation bietet weitere Informationen über Subscriptions