Full Reactive Stack – El frontend Angular
Construyendo un frontend Angular reactivo para nuestra aplicación Spring WebFlux
Este post es una versión de blog de una parte de la guía Full Reactive Stack with Spring Boot, WebFlux, Angular, RxJS, and Eventsource. El código fuente completo está en GitHub.
Esta guía tiene cuatro capítulos principales y un apéndice:
-
La Web Reactiva. Este capítulo te guía a través de los conceptos principales de un enfoque reactivo y lo compara con las interfaces web clásicas.
-
El backend. Esta sección cubre la creación de la aplicación Spring Boot usando las dependencias requeridas y el desarrollo de las capas del backend, prestando especial atención a los Controllers con WebFlux y el driver reactivo de MongoDB.
-
El frontend. Recorre el desarrollo de una aplicación Angular que consume el endpoint reactivo creado en el lado del servidor.
-
Conclusiones. Esta parte completa la guía, resumiendo lo que has construido y comparando los enfoques Web Reactivo y Clásico. Da algunas perspectivas sobre rendimiento, testabilidad, etc.
-
Ejecutar la app con Docker. Este apéndice te ayuda a construir y ejecutar la aplicación full-stack en Docker.
Todo el código fuente (spring-boot, Angular, Docker) está disponible en GitHub: repositorio Full-Reactive Stack. Si te resulta útil, por favor dale una estrella!
En el capítulo anterior, creamos una aplicación Spring Boot con una API Web Reactiva. En este capítulo, cubriremos cómo implementar el lado del cliente usando Angular con EventSource y Observable de RxJS.
Objetivo
Este capítulo se enfoca en el lado del cliente del sistema. Ya hemos creado un Endpoint Web Reactivo que puede suministrar server-sent events, pero ¿cómo podemos usarlo de manera más realista? Esa será nuestra aplicación Angular.
Como descargo de responsabilidad, estoy lejos de ser un experto en Angular y Typescript, así que la aplicación tiene margen de mejora. Saltaré los conceptos básicos de Angular e inmediatamente saltaré al Servicio Reactivo. Si quieres aprender Angular, esta guía no es la mejor opción; mejor revisa los documentos oficiales, por ejemplo.
¿Por qué Angular?
Elegí Angular para implementar el lado del frontend porque tengo algo de experiencia con este framework, y es muy amigable con el backend. Sin embargo, podrías usar las instrucciones en esta guía para implementar la solución en cualquier otro framework. Eso es porque la funcionalidad reactiva está respaldada por RxJS y la especificación EventSource, que es parte de HTML5.
Creando la aplicación Angular
La mejor opción para obtener el esqueleto del proyecto Angular es usar la herramienta Angular CLI, como se describe en la guía de Configuración en el sitio web de Angular.
También necesitarás Node.js (incluyendo npm) instalado en tu sistema.
Recuerda que el código fuente completo (Spring Boot, Angular, Docker) está disponible en GitHub: repositorio Full-Reactive Stack. Si te resulta útil, por favor márcalo con una estrella!
Resumen de la Aplicación UI
Construiremos una aplicación simple que nos permite cargar las citas disponibles del servidor de manera reactiva (usando Server-Sent Events) o en un estilo bloqueante clásico. Esa opción vendrá de nuestro Servicio Angular, así que implementaremos dos para mostrar las diferencias. La UI contiene una tabla, y el objetivo es que las filas de la tabla se actualicen tan pronto como recibamos citas del libro.
Para hacerlo más interesante, si hacemos clic en una cita, el contenido completo se mostrará en un componente diferente en la pantalla. Así que, es una vista maestro-detalle donde las filas maestras se cargan dinámicamente usando patrones Reactivos.
Como se muestra en la figura anterior, nuestra aplicación tiene tres componentes simples de Angular y dos servicios. El componente en la parte superior de la jerarquía es el App Component, que actúa solo como un envoltorio para el Quotes Component en nuestro escenario.
El Quotes Component es el componente central de nuestra aplicación. Usa el Quote Reactive Service y el Quote Blocking Service a través de Dependency Injection. El Quote Detail Component también estará incluido en el componente principal. Solo muestra el contenido completo del objeto Quote seleccionado.
El resto de los archivos en el repositorio de GitHub son tests, estilos, gestión de dependencias, definiciones de módulos, etc. Como mencioné antes, si no estás familiarizado con Angular y quieres aprender más, recomiendo que sigas el tutorial introductorio Tour of Heroes, en el que captarás todos estos conceptos.
Este capítulo se enfocará en cómo funciona el Quote Reactive Service y su conexión con el Component.
El servicio Angular Reactivo
Veamos el código del servicio reactivo que interactúa con el backend usando Server-Sent Events. Lo cubriremos en detalle más adelante.
import { Injectable } from '@angular/core';
import { Quote } from './quote';
import {Observable} from 'rxjs';
@Injectable()
export class QuoteReactiveService {
url: string = 'http://localhost:8080/quotes-reactive';
urlPaged: string = 'http://localhost:8080/quotes-reactive-paged';
getQuoteStream(page?: number, size?: number): Observable<Quote> {
return new Observable<Quote>((observer) => {
let url = this.url;
if (page != null) {
url = this.urlPaged + '?page=' + page + '&size=' + size;
}
let eventSource = new EventSource(url);
eventSource.onmessage = (event) => {
console.debug('Received event: ', event);
let json = JSON.parse(event.data);
observer.next(new Quote(json['id'], json['book'], json['content']));
};
eventSource.onerror = (error) => {
// readyState === 0 (closed) means the remote source closed the connection,
// so we can safely treat it as a normal situation. Another way
// of detecting the end of the stream is to insert a special element
// in the stream of events, which the client can identify as the last one.
if(eventSource.readyState === 0) {
console.log('The stream has been closed by the server.');
eventSource.close();
observer.complete();
} else {
observer.error('EventSource error: ' + error);
}
}
});
}
}
Ahora, alguna explicación sobre las ideas más importantes en ese código:
Con EventSource, nos conectamos al endpoint, estableciendo un Content-Type text/event-stream. Esto se configura implícitamente por la API. Como vimos en la sección anterior, abrimos un canal en el que el servidor nos enviará eventos. Creamos implícitamente esta conexión al construir el objeto, pasando el endpoint donde
Cuando recibimos un evento a través del objeto EventSource (onmessage), emitimos un nuevo objeto Quote usando Observable.next. Ten en cuenta que no usamos EventSource (SSE) para pasar datos dentro de nuestra aplicación UI. En su lugar, usamos aquí un objeto ReactiveX (RxJS) Observable. Un Subscriber de ese Observable recibirá eventos cuando el array se actualice – una acción que realizamos llamando a next().
Como se detalla en los comentarios del código, obtendremos un error cuando el remoto cierre el stream. Podemos identificar ese error si readyState es cero, y luego lo tratamos como una situación normal: completamos el observer para que los subscribers sepan que consumieron todos los datos disponibles.
Los Componentes Angular – Resumen Rápido
El Quotes Component (quotes.component.ts) es la parte que interactúa con los Servicios, tanto el bloqueante como el reactivo.
Este componente usa el servicio Reactivo o el Bloqueante dependiendo de la elección del usuario (vía botones en la página HTML, como veremos en la siguiente subsección). Se suscribe al objeto Observable y empuja un nuevo elemento en el array de datos cada vez que llega un nuevo artículo. Esa es la parte más crucial de la lógica del componente. Para asegurarnos de que la UI se refresque después de cada actualización, usamos el
ChangeDetectorRef de Angular vía inyección para forzar la detección de cambios.
Como puedes ver, no importa qué servicio estemos llamando, el resultado será un Observable. La razón es que la API HTTP bloqueante que usamos también devuelve un objeto Observable. La única diferencia es que el servicio bloqueante devuelve un array completo de Quotes de una vez.
import {Quote} from './quote';
import {QuoteReactiveService} from './quote-reactive.service';
import {QuoteBlockingService} from './quote-blocking.service';
import {Observable} from 'rxjs';
import {ChangeDetectorRef, Component} from "@angular/core";
@Component({
selector: 'app-component-quotes',
providers: [QuoteReactiveService],
templateUrl: './quotes.component.html'
})
export class QuotesComponent {
quoteArray: Quote[] = [];
selectedQuote: Quote;
mode: string;
pagination: boolean;
page: number;
size: number;
constructor(private quoteReactiveService: QuoteReactiveService, private quoteBlockingService: QuoteBlockingService, private cdr: ChangeDetectorRef) {
this.mode = "reactive";
this.pagination = true;
this.page = 0;
this.size = 50;
}
resetData() {
this.quoteArray = [];
}
requestQuoteStream(): void {
this.resetData();
let quoteObservable: Observable<Quote>;
if (this.pagination === true) {
quoteObservable = this.quoteReactiveService.getQuoteStream(this.page, this.size);
} else {
quoteObservable = this.quoteReactiveService.getQuoteStream();
}
quoteObservable.subscribe(quote => {
this.quoteArray.push(quote);
this.cdr.detectChanges();
});
}
requestQuoteBlocking(): void {
this.resetData();
if (this.pagination === true) {
this.quoteBlockingService.getQuotes(this.page, this.size)
.subscribe(q => this.quoteArray = q);
} else {
this.quoteBlockingService.getQuotes()
.subscribe(q => this.quoteArray = q);
}
}
onSelect(quote: Quote): void {
this.selectedQuote = quote;
}
}
Para completar el stack servicio-componente-plantilla, debemos proporcionar código HTML que renderice este componente. Esta implementación es muy simple ya que usa conceptos básicos de Angular con un for sobre un array. Mira el código a continuación.
<div class="row">
<div class="col-md-8">
<p>
<label>Pagination:
<input type="checkbox" [checked]="pagination" (change)="pagination = !pagination" >
<label>Page #:
<input type="text" [(ngModel)]="page" size="2">
</label>
<label>Page Size:
<input type="text" [(ngModel)]="size" size="2">
</label>
</label>
<button (click)="requestQuoteStream()">Reactive Request</button>
<button (click)="requestQuoteBlocking()">Blocking Request</button>
</p>
<table class="table">
<thead>
<tr>
<th>Quote ID</th>
<th>Book</th>
<th>Quote content</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let quote of quoteArray" [class.success]="quote === selectedQuote" (click)="onSelect(quote)">
<td>{{quote.id}}</td>
<td>{{quote.book}}</td>
<td>{{quote.content.substr(0, 30)}}...</td>
<tr>
</tbody>
</table>
</div>
<div class="col-md-4">
<div class="message">Click on a quote for more details</div>
<app-quote-detail [quote]="selectedQuote"></app-quote-detail>
</div>
</div>
Desbloquea la Guía Reactiva Completa: Obtén Esta Serie de Blog y Más como un eBook Completo—¡Disponible Ahora para Comprar! Descarga todos los posts (más bonus) como un mini-libro
Ejecutando el frontend
Primero, asegúrate de que el lado del backend esté arriba y funcionando, MongoDB incluido. Si clonaste el repositorio git, debes descargar las dependencias de node. Desde la carpeta angular-reactive, tienes que ejecutar:
$ npm install
Luego, podemos ejecutar la aplicación frontend desde la línea de comandos con:
$ npm run ng serve
Si todo va bien, veremos la siguiente pantalla cuando naveguemos con nuestro navegador a http://localhost:4200.
Tenemos dos botones para elegir entre una petición Reactiva o Bloqueante. Ese es el cambio entre llamar a un servicio u otro. También podemos decidir si queremos paginación y su configuración correspondiente.
Recuerda que, dependiendo de tu dispositivo, puedes experimentar problemas de memoria cuando solicites todos los datos simultáneamente (sin paginación). En ese caso, puedes cambiar el código del backend (controlador) para limitar el número de citas devueltas. Revisa el capítulo del backend para más detalles.
Ahora podemos jugar con todas estas opciones y ver cómo las citas se cargan ya sea de una vez o una por una, dependiendo de si ejecutamos una petición bloqueante o una reactiva.
Deberías poder sentir cómo la experiencia del usuario mejora con el enfoque reactivo. Las citas aparecerán tan pronto como los datos estén disponibles para que podamos acceder a ellas casi inmediatamente. No necesitamos esperar a que el servidor devuelva todos los elementos (o una página completa). En el siguiente capítulo, detallaremos el rendimiento y conclusiones adicionales.
App Angular Reactiva – Citas
Navega ahora al artículo de Conclusiones, o Desbloquea la Guía Reactiva Completa: Obtén Esta Serie de Blog y Más como un eBook Completo—¡Disponible Ahora para Comprar!
Continúa Leyendo Esta Serie
Este artículo es parte de la serie Full Reactive Stack:
- Parte 1: Introducción
- Parte 2: El Backend - Spring Boot, WebFlux y MongoDB
- Parte 3: El Frontend Angular (este post)
- Parte 4: Conclusiones
¿Quieres la guía completa? Obtén Esta Serie de Blog y Más como un eBook Completo—¡Disponible Ahora para Comprar!
Get More Architecture Insights
Join hundreds of software architects and engineers getting practical insights delivered to their inbox.