-
Notifications
You must be signed in to change notification settings - Fork 0
05 Services
- Ziele
- Was sind Services
- Dependency Injection
- Dependency Injection von Services im ng2 Framework
- Service Providers
- Annotation @Injectable()
- Asynchrone Services mittels Promises
- Zusammenfassung
- eigene Services schreiben, die Daten anbieten
- verstehen was Dependency Injection ist und was es damit zu tun hat
- Nutzung von Providers
- lernen wie asynchrone Services umgesetzt werden können
Services sind Klassen, die einen bestimmten Dienst anbieten. Normalerweise besteht dieser Dienst im Liefern und speichern von bestimmten Daten (sogenannten Model-Instanzen). Das simple Grundprinzip ist in der Abbildung dargestellt.

Über Services können Komponenten leicht miteinander Daten austauschen ohne sich direkt selbst gegenseitig zu kennen. Services werden üblicherweise mehrfach in der Anwendung verwendet.
In TypeScript schreiben wir uns einen simplen Service, der die Heroe-Daten aus dem fest hinterlegten Array in der Datei ./heroes-mockdata zurückliefert. Jetzt können Komponenten den Service nutzen, statt direkt das Array zu importieren. So kann später ausgetauscht werden, woher die Hero-Daten tatsächlich kommen. Nur der Service muss dazu später angepasst werden. hero.service.ts:
import { HEROES } from './heroes-mockdata';
export class HeroService {
getHeroes() {
return HEROES;
}
}Zur Erinnerung, hier die Daten in unserer hero-mockdata.ts:
import { Hero } from './hero';
export const HEROES: Hero[] = [
{id: 11, name: 'Mr. Nice', nickname: 'Nicy', weapon: 'Smilebow', noArms: false},
{id: 12, name: 'Narco', nickname: 'Nana', weapon: '', noArms: false},
{id: 13, name: 'Bombasto', nickname: 'Bomber', weapon: 'Bomb', noArms: false},
// usw.
]Um zu verstehen wie Services und Data Providers in AngularJS 2 funktionieren, ist eine kurze Einführung zu Dependency Injection (DI) nötig.
DI ist ein wichtiges Anwendungs-Design Muster und Angular bringt sein eigenes DI Framework mit. Es ist praktisch unmöglich eine Angular Anwendung ohne DI zu entwickeln. DI bedeutet eigentlich nichts anderes, als Abhängigkeiten zu konkreten Implementierungen aufzulösen indem man mit Interfaces, Constructor Injection oder Setter Injection arbeitet. Das sorgt dafür, dass die aufrufende Komponente oder Klasse die Abhängigkeiten an die aufgerufene Komponente oder Klasse zur Laufzeit übergibt und Abhängigkeiten somit nicht statisch z.B. im Constructor erzeugt werden.
Beispiel: In einem herkömmlichen Objekt-orientierten Programm ist jedes Objekt selbst dafür zuständig, seine Abhängigkeiten, also andere benötigte Objekte und Ressourcen, zu verwalten. Dafür muss jeden Objekt Kenntnisse seiner Umgebung haben, die es zur Erfüllung seiner Aufgabe eigentlich gar nicht benötigen würde. Ein Objekt würde also für eine Datenbankverbindung selbst einen SQLConnect instanzieren um beispielsweise darin etwas zu speichern: (some-class.ts)
private database: SQLConnect;
private template: Template;
public description = 'No DI';
constructor() {
this.database = new SQLConnect('https://…');
this.template = new WireTemplate();
}Der Nachteil hier ist die starre Kopplung, da der Konstruktor sich selbst die Instanzen von SQLConnect und WireTemplate erstellt. Er muss dazu nicht nur die konkreten Implementierungsklassen kennen (importieren), sondern auch wissen, mit welchen Konstruktor-Parametern diese wiederum aufgerufen werden. Ändert sich die Datenbank oder soll someClass mit einer Dummy-DB oder dummy-Template getestet werden, ist das umständlich zu machen (erfordert also Code-Änderungen).
besser: (some-class-neu.ts)
public description = 'DI';
constructor(private database: SQLConnect, private template: Template) {
…
}Jetzt sind die Abhängigkeiten (Dependencies) entkoppelt. Der Konstruktor von some-class erhält die notwendigen Objekte beim Aufruf von irgendeinem übergeordneten Code. Doch es geht sogar noch einfacher, per automatischer DI in einem Framework (nächster Abschnitt).
Ein sehr zu empfehlendes abstraktes Beispiel der DI ist Die Metapher mit dem Meister und dem Lehrling von Daniel Meixner
AngularJS 2 besitzt ein eigenes Injector-Modul, d.h. AngularJS erkennt, wenn sie in einem constructor() gerne eine Instanz bestimmter (Service)-Klassen hätten. Angular verwaltet Singletons dieser Serviceklassen automatisch und übergibt jeweils die benötigten an den constructor. Beispiel: wir schreiben die app.component.tsum:
import {HeroService} from './hero.service';
@Component({
selector: 'my-app',
styleUrls: ['app/app.component.css'],
templateUrl: 'app/app.component.html',
providers: [HeroService]})
export class AppComponent implements OnInit {
heroes: Hero [] = null;
constructor(private heroService: HeroService) { // (1)
…
}
ngOnInit(): void { // (2)
this.heroes = this.heroService.getHeroes();
}
}In (1) geben sie nun an, dass ihre AppComponent gerne einen HeroService im Konstruktor hätte. Angular macht das für sie und übergibt die eine Singleton-Instanz des HeroService.
In (2) implementiert die AppComponent nun das LiveCycle Event OnInit von AngularJS 2. Die Methode wird dann bei Iniatlisierung der AppComponent Komponente aufgerufen. Als Faustregel gilt: im Konstruktor so wenig wie möglich an initialisierungs-Operationen durchführen. Das erhöhrt die Flexibilität und Leichtgewichtigkeit, da Instanzen auch mal angelegt werden können (bspw. für Tests) ohne teure Initialisierungs-Operationen. Lagern Sie die Operationen in ngOnInit() aus, welches von AngularJS dann aufgerufen wird, nachdem das Objekt fertig erstellt wurde. Im Beispiel wird eine Referenz auf das Hero-Array in this.heroesgespeichert (siehe dazu die HeroService Klasse von weiter oben, die hier genutzt wird).
Im Detail:
ngOnInit()wird ausgeführt, nachdem AngularJS die AppCOmponent instanziert hat, das Template geparsed hat und alle @Input Parameter, die die Komponente braucht, initialisiert hat. Diese können dann in ngOnInit() verwendet werden. Unser Beispiel verwendet noch keine @Input Variablen, aber das kommt im Tutorial bei 4. Modularisierung. Weitere Infos zum Lifecycle siehe AngularJS 2 Doku.
Damit AngularJS die passenden Singleton-Instanzen in die Komponenten (constructor()) injezieren kann, muss AngularJS wissen, wo diese Instanzen der Services herkommen. Dazu können sie sogenannte Providersangeben, also Factories oder einfach KlassenNamen, die dann die Instanz(en) der Services liefern. Das ist sehr einfach. Damit es für unsere app.component.ts funktioniert, wird dies beispielsweise in der app.module.ts angegeben:
@NgModule({
imports: [ BrowserModule, FormsModule ],
declarations: [
AppComponent,
HeroDetailComponent,
…
],
providers: [HeroService], // (1)
bootstrap: [ AppComponent ]
})
export class AppModule { }Das Attribut providers: an Stelle (1) zeigt, wie ein Array von solchen Providern angegeben werden kann. Der AngularJS INjektor sucht ausgehend von der Nutzung einer DI solange die Baumhierarchie der Komponenten und Module nach oben ab, bis ein Provider gefunden wurde. Dieser wird genutzt. Unsere hero.component.ts möchte im constructor() eine HeroService Instanz haben, diese wird in der übergeordneten app.module.ts entsprechend gefunden.
Sollen Sie einmal nicht wünschen, dass alle Komponenten-Instanzen die gleiche eine Singleton Instanz Ihres Service erhalten, können Sie denProvider entsprechend auf tieferer Ebene angeben. Wollten wir beispielsweise, dass jede Instanz von app.component.ts eine eigene HeroService Instanz bekommt, ließe sich das wie folgt in der app.component.tsals provider angeben:
@Component({
selector: 'my-app',
…
providers: [HeroService]
})
export class AppComponent {
}Jetzt findet der Injektor von AngularJS den passenden Provider bereits direkt in der app.component.ts angegeben und nutzt diesen jeweils für app.component.ts Instanzen . Alle Child-Komponenten von app.component.ts bekommen weiterhin die gleiche Service-Instanz wie ihr Parent.
Dieses Konzept wird relevant, wenn Sie einen eigenen Undo/Redo-Service beispielsweise für Komponenten möchten, die mehrfach in Ihrem DOM vorkommen. Das nutzen wir jetzt gleich die Editierung einzelner Einträge in der HeroList

Um einen Hero in der Liste direkt editierbar zu machen, erstellen wir eine hero.card.component.ts die bei einem Klick auf edit den Namen als Eingabefeld anzeigt (das ist dann die hero.editor.component.ts. Da dies in der Liste mehrfach parallel auftreten kann, haben wir also viele Instanzen der hero.editor.component.ts.

Jede HeroEditorComponent soll ihren eigenen restore.service.ts erhalten, um erst nach eine Klick auf save die Änderungen in die Hero Instanz zu speichern. Dazu muss der RestoreService auf Ebene der HeroEditor-Komponente angegeben werden, damit jede ihre eigene Instanz erhält.
(Code-Listing der einzelnen Dateien finden Sie im Repository mit Tag 05_Services)
Für AngularJS 2 Komponenten funktioniert die Injektion von Abhängigkeiten (DI) automatisch. Sollten sie auch in ihren Services und anderen normalen TypeScript-Klassen Injektion für Konstruktoren wünschen, muss dies AngularJS mittels der Annotation @Injectable() mitgeteilt werden. Es ist daher guter Brauch, überall außerhalb von Komponenten -- insbersondere für Services -- diese Annotation sicherheitshalber schonmal gleich mit dranzuschreiben:
hero.service.ts mit fiktiver Erweiterung um einen Logger, der @Injectable() notwendig macht:
import { Injectable } from '@angular/core';
import { HEROES } from './heroes-mockdata';
import { Logger } from './logger.service';
@Injectable() // <- hier Pflicht, da constructor einen Logger injeziert haben will
export class HeroService {
constructor(private logger: Logger) {}
getHeroes() {
this.logger.log(“Getting heroes…”);
return HEROES;
}
}Bisher ist unser hero.service.ts noch blockierend, d.h. der Code wird synchron ausgeführt. Das macht derzeit nichts, da die Hero-Daten sowieso aus einem fertigen Mock-Array kommen und sofort im HeroService in getHeroes() zurückgegeben werden. Üblicherweise holen Services ihre Daten aber aus einem Backend vom Server mittels HTTP als JSON-Daten. Damit nun ihre Anwendung nicht einfriert, während dieser Zeit des Abrufens, bietet es sich an, Service-Methoden asynchron zu machen mittels Promises. hero.service.ts sieht dann so aus:
import { HEROES } from './heroes-mockdata';
export class HeroService {
getHeroes() {
return Promise.resolve(HEROES);
}
}und wird wie folgt in der app.component.ts verwendet:
...
ngOnInit() {
this.heroService
.getHeroes()
.then(heroes =>
this.heroes = heroes;
);
}Nun werden sie beim ausführen dieses Codes keinen Unterschied feststellen, da die Hero-Daten direkt aus dem Array sofort da sind und der Promise direkt erfüllt ist. Die Verzögerung des Ladens über eine langsame Internetverbindung läßt sich jedoch simulieren. Schreiben Sie dafür die getHeroes() ihres Services um:
import { HEROES } from './heroes-mockdata';
export class HeroService {
getHeroesFast() {
return Promise.resolve(HEROES);
}
getHeroes(): Promise<Hero[]> {
return new Promise<Hero[]>(resolve =>
setTimeout(resolve, 2000)) // delay 2 Sekunden
.then(() => { return this.getHeroes(); });
}
}Später im Tutorial werden wir die Hero-Daten tatsächlich über HTTP laden. Dann wird der asynchronen Aufruf mit Promises absolut notwendig (siehe 6 HTTP)
- Services sind dazu da, unserer App Daten (in der Regel durch AJAX-Aufrufe und Zurückgeben eines
Promise) bereitzustellen. - Services werden mittels
Dependency Injectionan unsere Komponenten übergeben, indem einProviderdafür registriert wird. - Kind-Komponenten einer Service-verwendenden Komponente haben automatisch Zugriff auf das gleiche
Singleton. - Services können eigene Dependencies haben und benötigen dafür den Decorator
@Injectable.
Den Quellcode zu diesem Stand des Tutorials finden Sie unter dem Tag 05_Services im Repository.
- https://angular.io/docs/ts/latest/tutorial/toh-pt4.html
- https://angular.io/docs/ts/latest/guide/dependency-injection.html
- https://angular.io/docs/ts/latest/guide/hierarchical-dependency-injection.html
- https://angular.io/docs/ts/latest/cookbook/ngmodule-faq.html#!#q-root-component-or-module
- https://angular.io/docs/ts/latest/guide/lifecycle-hooks.html
- https://angular.io/docs/ts/latest/guide/ngmodule.html
- https://s3-us-west-2.amazonaws.com/s.cdpn.io/68939/angular-logo.png
- http://blog.thoughtram.io/images/di-in-angular2-5.svg
- https://angularjs.de/artikel/angular2-tutorial-deutsch
- http://misko.hevery.com/code-reviewers-guide/flaw-constructor-does-real-work/
(c) 2017 Jonas Janczyk, Johannes Knauft, Johannes Konert, Samed Sulanc und weitere
(c) 2016 Jorge Ayala, Jules Döring, Alessandro Furkim, Mohamad Kamawall, Johannes Konert, Lennart Lehmann, Jonathan Stoye und weitere.
veröffentlicht unter MIT Lizenz.