Servicios
El componente HeroesComponent
de Tour de Héroes actualmente obtiene y muestra datos simulados.
Después de la refactorización de este tutorial, HeroesComponent
estará enfocado en dar soporte a la vista. Además, esto facilitará los tests unitarios a través de un servicio simulado (mock).
¿Por qué servicios?
Los componentes no debería recuperar o guardar datos directamente, y desde luego, no debería mostrar datos simulados. Debería dedicarse a sólo a presentar los datos y delegar el acceso de datos a un servicio.
En este tutorial, crearemos un servicio HeroService
que todas las clases de la aplicación podrán usar para obtener los héroes. En lugar de crear ese servicio con new
, usaremos la inyección de dependencias de Angular para inyectarlo en el constructor de HeroesComponent
.
Los servicios son una forma estupenda de compartir información entre clases que no se conocen. Crearemos un servicio MessageService
y lo inyectaremos en dos sitios:
- en
HeroService
que usa el servicio para enviar un mensaje. - en
MessagesComponent
que muestra el mensaje.
Crea HeroService
Usando el CLI de Angular, crea un servicio llamado hero
.
1 2 3 |
ng generate service hero |
Este comando genera un esqueleto de la clase HeroService
en rc/app/hero.service.ts
. El servicio HeroService
debería ser así:
1 2 3 4 5 6 7 8 |
import { Injectable } from '@angular/core'; @Injectable() export class HeroService { constructor() { } } |
Servicios @Injectable()
Observa que el nuevo servicio importa el símbolo de Angular Injectable
y anota la clase con el decorador @Injectable()
.
El decorador @Injectable()
le indica a Angular que este servicio puede tener dependencias inyectadas. Ahora mismo no tiene dependencias, pero las tendrá pronto. Tanto si tiene como si no, es una buena práctica mantener el decorador.
La guía de estilos de Angular recomienda encarecidamente mantenerlo y el optimizador (linter) obliga a respetar esta regla.
Obtener los datos del héroe
El servicio HeroService
puede obtener los datos de cualquier parte – un servicio web, almacenamiento local o un origen de datos simulado (mock).
Eliminar el acceso de dato de los componentes implica que puedas cambiar de idea acerca de la implmentación en cualquier momento, sin tener que modificar ningún componente. Estos desconocen el funcionamiento del servicio.
La implementación en este tutorial seguirá ofreciendo héroes simulados.
Importa hero y
HEROES
.
1 2 |
import { Hero } from './hero'; import { HEROES } from './mock-heroes'; |
Añade un método getHeroes
para devolver los héroes simulados.
1 2 3 |
getHeroes(): Hero[] { return HEROES; } |
Provee el servicio HeroService
Debemos proveer el servicio HeroService
en el sistema de inyección de dependencias antes de que Angular pueda inyectarlo en HeroesComponent
, como verás más abajo.
Hay diferentes maneras de proveer HeroService
: en HeroesComponent
, en AppComponent
y en AppModule
. Cada opción tiene sus pros y sus contras.
Este tutorial elige proveerlo en AppModule
.
Esta opción es tan habitual que podríamos haberle indicado al CLI que provea el servicio automáticamente en AppModule
añadiendo --module=app
.
1 2 3 |
ng generate service hero --module=app |
Como no lo hicimos así, habrá que proveerlo ahora manualmente.
Abre la clase AppModule
, importa HeroService
y añadelo al array @NgModule.providers
.
1 2 3 4 |
providers: [ HeroService, /* . . . */ ], |
El array providers
le indica a Angular que tiene que crear una única y compartida instancia de HeroService
e inyectarla en cualquier clase que lo solicite.
HeroService
ya está listo para conectarse con HeroesComponent
.
Este es un ejemplo de código provisional que nos va a permitir proveer y usar el servicio
HeroService
. En este punto, el código será diferente del deHeroService
en la revisión final de código de esta página
Actualiza HeroesComponent
Abre el fichero de la clase HeroesComponent
.
Borra la importación de HEROES
ya que no la vamos a necesitar más. Importa el servicio HeroService
en su lugar.
1 |
import { HeroService } from '../hero.service'; |
Reemplaza la definición de la propiedad heroes
por una simple declaración.
1 |
heroes: Hero[]; |
Inyecta HeroService
Añade un parámetro privado llamado heroService
y de tipo HeroService
al constructor.
1 |
constructor(private heroService: HeroService) { } |
El parámetro define simultáneamente una propiedad privada heroService
y lo identifica como una inyección HeroService
.
Cuando Angular crea un HeroComponent
, el sistema de Inyección de Dependencias establece el parámetro heroService
como la instancia única (singleton) de HeroService
.
Añade getHeroes()
Crea una función que recupere los héroes del servicio.
1 2 3 |
getHeroes(): void { this.heroes = this.heroService.getHeroes(); } |
Llámala en ngOnInit
Aunque podrías llamar a getHeroes()
en el constructor, eso no es una buena práctica.
Reserva el constructor para inicializaciones simples, como asignar los parámetros del constructor a la propiedades. El constructor no debería hacer nada. A buen seguro, no debería llamar a una función que hace peticiones HTTP a un servidor remoto, como el haría un servicio real.
En su lugar, llama a getHeroes()
dentro del ‘enganche del ciclo de vida’ (lifecycle hook) de ngOnInit y deja que Angular llame a ngOnInit
en el momento apropiado después de construir una instancia de HeroesComponent
.
1 2 3 |
ngOnInit() { this.getHeroes(); } |
Comprueba como se ejecuta
Después de que el navegador se actualice, la aplicación debería ejecutarse como antes, mostrando un listado de héroes y el detalle del héroe sobre el que hagamos clic.
Datos Observable
El método HeroService.getHeroes()
tiene una firma síncrona, lo que implica que HeroService
puede recuperar héroes síncronamente. HeroesComponent
consume el resultado de getHeroes()
como si los héroes pudieran ser recuperados síncronamente.
1 |
this.heroes = this.heroService.getHeroes(); |
Esto no funcionaría en una aplicación real. Ahora funciona porque el servicio actualmente devuelve héroes simulados. Pero pronto la aplicación recuperará los héroes desde un servidor remoto, lo cual es una operación asíncrona.
HeroService
debe esperar a que el servidor responda, getHeroes()
no puede devolver datos inmediatamente y el navegador no se bloqueará mientras el servicio espera.
HeroService.getHeroes()
debe tener algún tipo de firma asíncrona.
Puede aceptar una retrollamada (callback). Puede devolver una Promesa
. Puede devolver un Observable
.
En este tutorial HeroService.getHeroes()
va a devolver un Observable
. En parte porque finalmente usará el método de Angular HttpClient.get
para recuperar los héroes y HttpClient.get
devuelve un Observable
.
Observable HeroService
Observable
es una de las clases clave de la librería RxJS.
En el próximo tutorial HTTP, aprenderemos que los métodos de HttpClient
de Angular devuelven Observable
s de RxJS. En este tutorial simularemos la obtención de datos de un servidor con la función de RxJS of()
.
Abre el fichero HeroService
e importa los símbolos Observable
y of
de RxJS.
1 2 |
import { Observable } from 'rxjs/Observable'; import { of } from 'rxjs/observable/of'; |
Reemplaza el método getHeroes()
por este.
1 2 3 |
getHeroes(): Observable<Hero[]> { return of(HEROES); } |
of(HEROES)
devuelve un Observable<Hero[]>
que emite un único valor, el array de héroes simulado.
En el tutorial HTTP llamaremos a
HttpClient.get<Hero[]>()
el cual también devuelve unObservable[]
que emite un único valor, un array de héroes dentro del cuerpo de la respuesta HTTP
Suscribe a HeroesComponent
El método HeroService.getHeroes()
antes retornaba Hero[]
. Ahora devuelve un Observable[]
.
Tendrás que modificar HeroesComponent
para que se ajuste a ese cambio.
Encuentra el método getHeroes()
y reemplázalo con el siguiente código (se muestra también la versión anterior para su comparación)
1 2 3 4 |
getHeroes(): void { this.heroService.getHeroes() .subscribe(heroes => this.heroes = heroes); } |
1 2 3 |
getHeroes(): void { this.heroes = this.heroService.getHeroes(); } |
La diferencia principal es Observable.subscribe()
La versión anterior asigna un array de héroes a la propiedad heroes
del componente. La asignación ocurre síncronamente, como si el servidor pudiera devolver héroes instantáneamente o el navegador pudiera bloquear la interfaz del usuario mientras espera la respuesta del servidor.
Esto no funcionará cuando HeroService
en realidad está haciendo peticiones a un servidor remoto.
La nueva versión espera que un Observable
emita un array de héroes – lo cual puede suceder ahora o dentro de varios minutos. Entonces, suscribe
para el array emitido a la retrollamada, la cual establece la propiedad heroes del componente.
Este enfoque asíncrono funcionará cuando HeroService
solicite héroes al servidor.
Mostrar mensajes
En está sección:
- añadiremos
MessagesComponent
que muestre mensajes de la aplicación al final de la pantalla. - crearemos un
MessageService
inyectable y que abarque toda la aplicación para enviar los mensajes a mostrar. - inyectaremos
MessageService
enHeroService
. - mostraremos un mensaje cuando
MessageService
recupere con éxito los héroes.
Crear MessagesComponent
Usa el CLI para crear el componente MessagesComponent
1 2 3 |
ng generate component messages |
El CLI crea los archivos del componente en la carpeta src/app/messages
y declara MessagesComponent
en AppModule
.
Modifica la plantilla AppComponent
para mostrar el componente MessagesComponent
generado.
1 2 3 |
<h1>{{title}}</h1> <app-heroes></app-heroes> <app-messages></app-messages> |
Deberías ver el texto por defecto de MessagesComponent
al final de la página.
Crea MessageService
Usa el CLI para generar MessageService
en src/app
. La opción --module=app
le indica al CLI que proporcione este servicio en AppModule
.
1 2 3 |
ng generate service message --module=app |
Abre MessageService
y reemplaza su contenido por el siguiente.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import { Injectable } from '@angular/core'; @Injectable() export class MessageService { messages: string[] = []; add(message: string) { this.messages.push(message); } clear() { this.messages = []; } } |
Este servicio expone su caché de mensajes y dos métodos: uno es add()
para añadir un mensaje a la caché y el otro es clear()
, para vaciar la caché.
Inyéctalo en HeroService
Vuelve a abrir HeroService
e importa MessageService
.
1 |
import { MessageService } from './message.service'; |
Modifica el constructor con un parámetro que declare una propiedad privada messageService
. Angular inyectará la instancia única (singleton) MessageService
en esa propiedad cuando crea HeroService
.
1 |
constructor(private messageService: MessageService) { } |
Este es un típico escenario ‘servicio-en-servicio’ : inyectamos
MessageService
enHeroService
el cual está inyectado enHeroesComponent
Enviar un mensaje desde HeroService
Modifica el método getHeroes()
para que envíe un mensaje cuando se recuperan los héroes.
1 2 3 4 5 |
getHeroes(): Observable<Hero[]> { // Todo: send the message _after_ fetching the heroes this.messageService.add('HeroService: fetched heroes'); return of(HEROES); } |
Muestra el mensaje desde HeroService
MessagesComponent
debería mostrar todos los mensajes, incluyendo el mensaje enviado por HeroService
cuando recupera héroes.
Abre MessagesComponent
e importa MessageService
1 |
import { MessageService } from '../message.service'; |
Modifica el constructor con un parámetro que declare una propiedad pública messageService
. Angular inyectará la instancia única (singleton) MessageService
en esa propiedad cuando crea HeroService
.
1 |
constructor(public messageService: MessageService) {} |
La propiedad messageService
debe ser pública porque vamos a vincularla en una plantilla.
Angular sólo permite vincular con las propiedades públicas de los componentes.
Vincular con MessageService
Reemplaza la plantilla generada por el CLI MessagesComponent
por lo siguiente.
1 2 3 4 5 6 7 8 |
<div *ngIf="messageService.messages.length"> <h2>Messages</h2> <button class="clear" (click)="messageService.clear()">clear</button> <div *ngFor='let message of messageService.messages'> {{message}} </div> </div> |
Esta plantilla se vincula directamente con messageService
del componente.
*ngIf
sólo muestra el área de mensajes si hay mensajes que mostrar.*ngFor
presenta el listado de mensajes en elementos<div>
repetidos.- Un evento de vinculación de Angular vincula el evento clic de un botón a
MessageService.clear()
.
Los mensajes tendrán un mejor aspecto cuando añadas los estilos CSS privados, tal y como verás en la revisión final de código.
El navegador se actualizará y la página mostrará un listado de héroes. Haz scroll hasta el final para ver el mensaje de HeroService
en el área de mensajes. Haz clic en el botón de ‘clear’ y el área de mensajes desparecerá.
Revisión final de código
Aquí está el código de los ficheros tratados en esta página y tu aplicación debería parecerse a este ejemplo / descarga ejemplo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import { Injectable } from '@angular/core'; import { Observable } from 'rxjs/Observable'; import { of } from 'rxjs/observable/of'; import { Hero } from './hero'; import { HEROES } from './mock-heroes'; import { MessageService } from './message.service'; @Injectable() export class HeroService { constructor(private messageService: MessageService) { } getHeroes(): Observable<Hero[]> { // Todo: send the message _after_ fetching the heroes this.messageService.add('HeroService: fetched heroes'); return of(HEROES); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import { Injectable } from '@angular/core'; @Injectable() export class MessageService { messages: string[] = []; add(message: string) { this.messages.push(message); } clear() { this.messages = []; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
import { Component, OnInit } from '@angular/core'; import { Hero } from '../hero'; import { HeroService } from '../hero.service'; @Component({ selector: 'app-heroes', templateUrl: './heroes.component.html', styleUrls: ['./heroes.component.css'] }) export class HeroesComponent implements OnInit { selectedHero: Hero; heroes: Hero[]; constructor(private heroService: HeroService) { } ngOnInit() { this.getHeroes(); } onSelect(hero: Hero): void { this.selectedHero = hero; } getHeroes(): void { this.heroService.getHeroes() .subscribe(heroes => this.heroes = heroes); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import { Component, OnInit } from '@angular/core'; import { MessageService } from '../message.service'; @Component({ selector: 'app-messages', templateUrl: './messages.component.html', styleUrls: ['./messages.component.css'] }) export class MessagesComponent implements OnInit { constructor(public messageService: MessageService) {} ngOnInit() { } } |
1 2 3 4 5 6 7 8 |
<div *ngIf="messageService.messages.length"> <h2>Messages</h2> <button class="clear" (click)="messageService.clear()">clear</button> <div *ngFor='let message of messageService.messages'> {{message}} </div> </div> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
/* MessagesComponent's private CSS styles */ h2 { color: red; font-family: Arial, Helvetica, sans-serif; font-weight: lighter; } body { margin: 2em; } body, input[text], button { color: crimson; font-family: Cambria, Georgia; } button.clear { font-family: Arial; background-color: #eee; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer; cursor: hand; } button:hover { background-color: #cfd8dc; } button:disabled { background-color: #eee; color: #aaa; cursor: auto; } button.clear { color: #888; margin-bottom: 12px; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { HeroesComponent } from './heroes/heroes.component'; import { HeroDetailComponent } from './hero-detail/hero-detail.component'; import { HeroService } from './hero.service'; import { MessageService } from './message.service'; import { MessagesComponent } from './messages/messages.component'; @NgModule({ declarations: [ AppComponent, HeroesComponent, HeroDetailComponent, MessagesComponent ], imports: [ BrowserModule, FormsModule ], providers: [ HeroService, MessageService ], bootstrap: [ AppComponent ] }) export class AppModule { } |
1 2 3 |
<h1>{{title}}</h1> <app-heroes></app-heroes> <app-messages></app-messages> |
Resumen
- Hemos refactorizado el acceso a datos en la clase
HeroService
- Hemos proporcionado
HeroService
en elAppModule
raíz, de modo que se pueda inyectar en cualquier lugar. - Hemos usado la Inyección de Dependencias de Angular para inyectarlo en un componente
- Le hemos dado al método de obtención de datos de
HeroService
una firma asíncrona. - Hemos descubierto
Observable
y la librería RxJS Observable - Hemos usado
of()
de RxJS para devolver un Observable de héroes simulados(Observable<Hero[]>)
. - El ‘enganche de ciclo de vida’ (lifecycle hook)
ngOnInit
del componente llama al método deHeroService
, no el contructor. - Hemos creado un
MessageService
para la comunicación desacoplada entre clases. - EL servicio
HeroService
inyectado en un componente se crea con otro servicio inyectado,MessageService
Nota: puedes encontrar el documento original de esta entrada en https://angular.io/tutorial/toh-pt4