Servicio mensajes Angular

Mario González - Blog sobre desarrollo web

Cómo programar un proveedor de mensajes para Angular con RxJS

Cuando estamos desarrollando una aplicación con Angular, lo normal es que tengamos el código repartido en varias piezas (componentes y servicios, sobre todo). Angular nos provee de mecanismos para que los componentes se comuniquen entre sí, tanto de padres a hijos como de hijos a padres. Estos mecanismos son muy útiles, pero cuando la jerarquía es compleja y queremos comunicaciones no lineales, probablemente no nos servirán.

En muchas ocasiones lo que necesitaremos es que nuestros componentes (o servicios) se puedan comunicar entre sí independientemente de qué lugar ocupen en la jerarquía. De hecho, a veces querremos que un componente emita un mensaje y que lo reciban varios componentes a la vez.

En este artículo aprenderemos a programar un simple proveedor de mensajes, que estará implementado dentro de un servicio. Así, cada componente que quiera emitir un mensaje o escuchar los mensajes que emiten los demás, sólo tendrá que incluir al servicio proveedor de mensajes y utilizar sus métodos públicos para uno u otro objetivo.

En este repositorio tenéis todo el código que desarrollaremos en el tutorial.

RxJS y el objeto BehaviorSubject

Como ya sabrás a estas alturas, las librerías RxJS nos sirven para trabajar con flujos asíncronos de datos mediante el uso de objetos de tipo Observable (¿mande?). Un mensaje que se puede emitir en cualquier momento durante la ejecución de la aplicación es un ejemplo perfecto de dato que forma parte de un flujo asíncrono.

En RxJS tenemos un objeto que nos vendrá de maravilla para implementar el emisor de mensajes: el BehaviorSubject. Este objeto es un tipo específico de objeto Subject. El Subject es una clase de Observable que nos permite tanto suscribirnos a él como hacerle emitir datos (o mensajes en nuestro caso), y el BehaviorSubject es un tipo de Subject que siempre emite un valor inicial al crearse y que permite que en el momento de la suscripción se reciba el último valor que emitió.


// BehaviorSubject que emite un valor inicial al crearse
let bs = new BehaviorSubject('valor-inicial');

// Podemos suscribirnos a él
bs.subscribe(msj => console.log(msj));

// Podemos hacer que emita un valor
bs.next('valor');

En el código anterior se imprimirán en consola dos mensajes: en el momento de la suscripción, porque a pesar de que el BehaviorSubject ya había emitido el mensaje anteriormente, dicho mensaje
se recibe; y después en la emisión del segundo mensaje.

El emisor de mensajes

Vamos a arremangarnos y a adaptar el código anterior para implementar un simple emisor de mensajes.

Lo primero que vamos a ver es cómo tipar el BehaviorSubject. Tendremos que decidir cuál va a ser el tipo de los mensajes. En el ejemplo anterior se emitían mensajes de tipo cadena, con lo que se hubiera tipado así:


let bs: BehaviorSubject<string> = new BehaviorSubject('valor-inicial');

Fijaos que usamos los genéricos de TypeScript para decir de qué tipo será el mensaje que se enviará.

Si bien los mensajes pueden ser de tipo cadena, cuando nuestra aplicación tenga muchos componentes enviándose mensajes entre sí, lo más lógico es que queramos organizarlos en temas. Imaginad, por ejemplo, un componente de un listado de películas. A este componente le interesa saber cuándo se ha añadido una película nueva a la base de datos, pero le dan exactamente igual los mensajes que tengan que ver, por ejemplo, con los datos del perfil de usuario. Si cada mensaje lleva asociado un tema, podemos hacer que los componentes sólo escuchen los mensajes del tema que les interesa.

Por tanto, vamos a hacer que nuestros mensajes tengan forma de objeto JavaScript con dos propiedades: tema y contenido.


// Creamos una interfaz que nos servirá para definir la forma del objeto mensaje
interface Mensaje {
	tema: string;
	contenido: string;
}

// Tipamos el BehaviorSubject con la interfaz anterior
let mensajero: BehaviorSubject<Mensaje> = new BehaviorSubject({
	tema: '',
	contenido: ''
});

Con esto ya tenemos creado nuestro objeto mensajero, que por ser un BehaviorSubject, tiene dos métodos que usaremos más adelante: subscribe() y next(). Fijaos que hemos tenido que emitir un mensaje vacío, pero que tiene la forma que Angular espera.

El emisor de mensajes como servicio de Angular

Y hablando de Angular, ¿en qué parte de nuestra aplicación metemos este código? Como dijimos más arriba, el proveedor de mensajes va a ser un servicio, de este modo cualquier componente (o también cualquier otro servicio) podrá tener acceso a él. Vamos, pues, a crear el servicio y a incluir el código que ya teníamos.

mensajes.service.ts

import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs'; // Tenemos que importar el objeto BehaviorSubject de la librería RxJS

// Interfaz para la forma del objeto mensaje
interface Mensaje {
	tema: string;
	contenido: string;
}

@Injectable({
	providedIn: 'root' // Así se establece a partir de Angular 6 el ámbito de la instancia del servicio
})
export class MensajesService {
	// El BehaviorSubject será una propiedad privada de la clase
	private mensajero: BehaviorSubject<Mensaje> = new BehaviorSubject({
		tema: '',
		contenido: ''
	});
}

Aparte del esqueleto inicial de cualquier servicio de Angular (fíjaos que en Angular 6 ha cambiado la manera de establecer el ámbito de la instancia de un servicio), tenemos que importar el objeto BehaviorSubject de las librerías RxJS. Afortunadamente su versión 6 ha simplificado la manera de importar los objetos y los operadores.

El objeto mensajero tiene visibilidad private porque implementaremos dos métodos públicos para acceder a las dos únicas cosas que deberían poderse ver desde fuera: la suscripción al objeto y la emisión de mensajes. Vamos con ellos:

mensajes.service.ts

import { Injectable } from '@angular/core';
import { Observable, BehaviorSubject } from 'rxjs'; // Tenemos que importar los observables de la librería RxJS

// Interfaz para la forma del objeto mensaje
interface Mensaje {
	tema: string;
	contenido: string;
}

@Injectable({
	providedIn: 'root' // Así se establece a partir de Angular 6 el ámbito de la instancia del servicio
})
export class MensajesService {
	private mensajero: BehaviorSubject<Mensaje> = new BehaviorSubject({
		tema: '',
		contenido: ''
	});

	constructor() { }

	// Método público para quien se quiera suscribir a los mensajes
	public escucha(): Observable<Mensaje> {
		return this.mensajero.asObservable();
	}

	// Método público para quien quiera emitir un mensaje
	public emite(msj: Mensaje): void {
		this.mensajero.next(msj);
	}
}

No queremos que el objeto BehaviorSubject sea directamente accesible. Para esto tenemos un método, asObservable(), que nos permite suscribirnos a él indirectamente. Es este método el que hacemos público.

Utilizando el proveedor de mensajes para emitir mensajes

Ya que tenemos una primera versión de nuestro servicio proveedor de mensajes, vamos a ver cómo podemos usarlo desde los componentes. Vamos a implementar un componente que emite un mensaje en su inicialización y vuelve a emitir otro dos segundos después. Más tarde, desde otro componente escucharemos esos mensajes y haremos que el componente reaccione a ellos.

temporizador.component.ts

import { Component, OnInit } from '@angular/core';
import { MensajesService } from './mensajes.service';  // Nuestro proveedor de mensajes

@Component({
	selector: 'msjs-temporizador',
	template: ''
})
export class TemporizadorComponent implements OnInit {
	constructor(private mensajesService: MensajesService) { }  // Inyectamos nuestro servicio de mensajes
	
	ngOnInit(): void {
		// Emitimos un mensaje al inicio
		this.mensajesService.emite({
			tema: 'tiempo',
			contenido: 'inicio'
		});
		setTimeout(() => {
			// Emitimos otro mensaje 2 segundos después
			this.mensajesService.emite({
				tema: 'tiempo',
				contenido: 'fin'
			})
		}, 2000);
	}
}

Analicemos el código anterior. Nuestro proveedor de mensajes es un servicio, por lo que si este componente quiere utilizarlo, debemos inyectárselo en el constructor. Una vez hecho esto, tendremos a nuestra disposición los dos métodos públicos del servicio: escucha() y emite().

Este componente llamará al método emite() dos veces: una al inicializarse y otra 2 segundos después. El método espera que le pasemos el mensaje: un objeto JS con dos propiedades, tema y contenido.

Fijaos en un detalle importante: el componente emite el mensaje, pero no sabe quién lo va a escuchar, respetando así el SRP o Principio de Responsabilidad Única. Cada componente que esté interesado en escuchar estos mensajes, tendrá que suscribirse al emisor de mensajes, y tampoco sabrá nada de quién emite dichos mensajes.

Utilizando el proveedor de mensajes para escuchar mensajes

Vamos a crear ahora un componente que escuche los mensajes y que reaccione ante ellos. Como vimos al principio del artículo, el emisor de mensajes es un tipo de Observable, por tanto para poder escuchar los mensajes emitidos por el proveedor de mensajes, o dicho de otra manera, 
para poder observar el flujo asíncrono de datos que emitirá el Observable, nos tenemos que suscribir a él. Y para poder hacer esto ya programamos un método público en el servicio, escuchar(), que devuelve el Observable para poder suscribirnos a él.

La suscripción a los mensajes la haremos al inicializarse el componente, es decir, en el método ngOnInit().

avisos.component.ts

import { Component, OnInit } from '@angular/core';
import { MensajesService } from './mensajes.service';  // Nuestro proveedor de mensajes

@Component({
	selector: 'msjs-avisos',
	template: ''  // Lo haremos después
})
export class AvisosComponent implements OnInit {
	constructor(private mensajesService: MensajesService) { }  // Inyectamos nuestro servicio de mensajes
	
	private escuchaMensajes(): void {
		this.mensajesService.escucha().subscribe();
	}
	
	ngOnInit(): void {
		this.escuchaMensajes();
	}
}

Como el método escucha() devuelve el Observable, podemos directamente llamar a su método subscribe(). Tal y como está ahora, estará escuchando mensajes pero no hará nada con ellos. Vamos a implementar la reacción a los mensajes:

avisos.component.ts

import { Component, OnInit } from '@angular/core';
import { MensajesService } from './mensajes.service';  // Nuestro proveedor de mensajes

@Component({
	selector: 'msjs-avisos',
	template: ''
})
export class AvisosComponent implements OnInit {
	constructor(private mensajesService: MensajesService) { }  // Inyectamos nuestro servicio de mensajes
	
	private escuchaMensajes(): void {
		this.mensajesService.escucha().subscribe(
			msj => {
				if (msj.tema === 'tiempo') {
					switch (msj.contenido) {
						case 'inicio':
							console.log('Se ha recibido la marca inicial de tiempo');
							break;
						case 'fin':
							console.log('Se ha recibido la marca final de tiempo');
							break;
					}
				}
			}
		);
	}
	
	ngOnInit(): void {
		this.escuchaMensajes();
	}
}

Analicemos lo que hemos añadido. La suscripción recibe, cada vez que un mensaje es emitido, un objeto de tipo Mensaje, que almacenamos en una variable local llamada msj. Como el mensaje tiene una propiedad llamada tema, y a este componente sólo le interesa escuchar los mensajes del tema tiempo, preguntamos primero por dicha propiedad. Después, dependiendo del contenido del mensaje, implementamos una reacción u otra (en este simple ejemplo nos limitamos a imprimir en consola).

Cancelar las suscripciones

Como práctica recomendable, recordad cancelar las suscripciones en Angular, para evitar memory leaks (fugas de memoria). Hay algunas excepciones a esta recomendación, como el Router y las peticiones HTTP, que se cancelan solas, pero en nuestra implementación tenemos que cancelarlas nosotros.

Veamos cómo se cancelan las suscripciones:

avisos.component.ts

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';  // Importamos el objeto Subscription de rxjs
import { MensajesService } from './mensajes.service';  // Nuestro proveedor de mensajes

@Component({
	selector: 'msjs-avisos',
	template: ''
})
export class AvisosComponent implements OnInit, OnDestroy {
	private suscripcionMensajes: Subscription; // Aquí almacenaremos la suscripción
	
	constructor(private mensajesService: MensajesService) { }  // Inyectamos nuestro servicio de mensajes
	
	private escuchaMensajes(): void {
		this.suscripcionMensajes = this.mensajesService.escucha().subscribe(
			msj => {
				if (msj.tema === 'tiempo') {
					switch (msj.contenido) {
						case 'inicio':
							console.log('Se ha recibido la marca inicial de tiempo');
							break;
						case 'fin':
							console.log('Se ha recibido la marca final de tiempo');
							break;
					}
				}
			}
		);
	}
	
	ngOnInit(): void {
		this.escuchaMensajes();
	}

	ngOnDestroy(): void {
		this.suscripcionMensajes.unsubscribe();  // Cancelamos la suscripción cuando se destruya el componente
	}	
}

Como veis, lo primero que tenemos que hacer es importar el objeto Subscription de las librerías RxJS y crear una variable local de tipo Subscription. En esta variable almacenaremos lo que nos devuelve el método subscribe() del observable emisor de mensajes (de hecho, devuelve un objeto de tipo Subscription).

El objeto Subscription tiene un método para cancelarse, unsubscribe(). El lugar para llamar a este método es el hook ngOnDestroy(), es decir, cuando se destruye el componente.

El método filter() de RxJS

En el anterior componente hemos filtrado los mensajes por tema. Esto quiere decir que el componente recibe todos los mensajes, sean del tema que sean, y sólo reacciona ante los de un tema determinado. Podemos sacar del componente la lógica del filtro y llevárnosla al servicio proveedor de mensajes. De este modo, al componente sólo le llegarán los mensajes del tema que le interese. Para ello modificaremos un poco el método escuchar(), al que ahora le tendremos que pasar una cadena con el tema al que nos queremos suscribir.

mensajes.service.ts

// Método público para quien se quiera suscribir a los mensajes
public escucha(tema: string): Observable<Mensaje> {
	return this.mensajero.asObservable().pipe(
		filter(msj => msj.tema === tema)
	);
}

Lo que hemos hecho es filtrar la emisión de mensajes desde el propio observable. Cuando un componente se suscriba para escuchar mensajes, llamará al método escucha() pasándole el tema que le interesa, y el observable sólo le emitirá los mensajes de ese tema.

RxJS ha cambiado en su versión 6 la manera de concatenar los operadores, ahora se hace llamando al método pipe() y añadiendo los operadores como argumentos de este método. Aquí podéis leer una explicación.

Ahora modificaremos nuestro componente para utilizar la nueva versión del método escucha().

avisos.component.ts

private escuchaMensajes(): void {
	this.suscripcionMensajes = this.mensajesService.escucha('tiempo').subscribe(  // Al llamar a escucha() le pasamos el tema que nos interesa
		msj => {
			switch (msj.contenido) {
				case 'inicio':
					console.log('Se ha recibido la marca inicial de tiempo');
					break;
				case 'fin':
					console.log('Se ha recibido la marca final de tiempo');
					break;
			}
		}
	);
}

Hemos podido quitar el if anterior, el que filtraba por tema, porque ahora tenemos la garantía de que este componente sólo va a recibir los mensajes del tema tiempo, así que sólo tenemos que fijarnos en el contenido.

Añadiendo interactividad a nuestro ejemplo

Con el código que tenemos ya podemos ver el funcionamiento del proveedor de mensajes, cómo darle la orden de que emita mensajes y cómo suscribirse a él para escucharlos. No obstante, ahora mismo nuestra aplicación es una página en blanco que imprime mensajes en consola. Vamos a añadirle un poco de interactividad.

Nuestro componente Temporizador se limitaba a emitir dos mensajes de manera automática. Vamos a modificarlo para que estos mensajes se envíen cuando el usuario pulse los botones correspondientes.

temporizador.component.ts

import { Component, OnInit } from '@angular/core';
import { MensajesService } from '../mensajes.service';  // Nuestro proveedor de mensajes

@Component({
	selector: 'msjs-temporizador',
	templateUrl: './temporizador.component.html'
})
export class TemporizadorComponent implements OnInit {
	public temporizadorIniciado = false;

	constructor(private mensajesService: MensajesService) { }  // Inyectamos nuestro servicio de mensajes

	public iniciarTemporizador(): void {
		this.temporizadorIniciado = true;
		this.mensajesService.emite({
			tema: 'tiempo',
			contenido: 'inicio'
		});
	}

	public finalizarTemporizador(): void {
		this.temporizadorIniciado = false;
		this.mensajesService.emite({
			tema: 'tiempo',
			contenido: 'fin'
		});
	}
}

Y el template:

temporizador.component.html

<button *ngIf="!temporizadorIniciado" (click)="iniciarTemporizador()">Iniciar</button>
<button *ngIf="temporizadorIniciado" (click)="finalizarTemporizador()">Finalizar</button>

Los estilos CSS no los veremos en el tutorial, pero los podéis consultar en el repositorio.

Básicamente hemos creado un botón para iniciar un supuesto temporizador y otro para finalizarlo. Cada botón llama a un método de nuestro componente, y esos dos métodos iniciarTemporizador() y finalizarTemporizador() son los encargados de emitir sendos mensajes.  A diferencia del ejemplo anterior, donde los mensajes se emitían automáticamente, ahora lo hacen a partir de una acción del usuario.

Lo siguiente que haremos será modificar el componente Avisos para que muestre un aviso por pantalla en vez de en la consola.

avisos.component.ts

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';  // Importamos el objeto Subscription de rxjs
import { MensajesService } from '../mensajes.service';  // Nuestro proveedor de mensajes

@Component({
	selector: 'msjs-avisos',
	templateUrl: './avisos.component.html',
	styleUrls: ['./avisos.component.css']
})
export class AvisosComponent implements OnInit, OnDestroy {
	public aviso = '';
	private suscripcionMensajes: Subscription; // Aquí almacenaremos la suscripción

	constructor(private mensajesService: MensajesService) { }  // Inyectamos nuestro servicio de mensajes

	private escuchaMensajes(): void {
		this.suscripcionMensajes = this.mensajesService.escucha('tiempo').subscribe(  // Al llamar a escucha() le pasamos el tema que nos interesa
			msj => {
				switch (msj.contenido) {
					case 'inicio':
						this.muestraAviso('Marca inicial');
						break;
					case 'fin':
						this.muestraAviso('Marca final');
						break;
				}
			}
		);
	}

	private muestraAviso(aviso: string): void {
		this.aviso = aviso;
		setTimeout(() => {
			this.aviso = '';
		}, 2000);
	}

	ngOnInit(): void {
		this.escuchaMensajes();
	}

	ngOnDestroy(): void {
		this.suscripcionMensajes.unsubscribe();  // Cancelamos la suscripción cuando se destruya el componente
	}	
}

Y el template:

avisos.component.html

<p [class.visible]="aviso != ''">{{aviso}}</p>

Como vemos, este componente se limitará a mostrar y ocultar un aviso de texto cada vez que recibe un mensaje.

Pulsa el botón para ver cómo los dos componentes se comunican entre sí

Resumen final

Hemos visto cómo implementar un proveedor de mensajes para que los componentes y servicios de nuestra aplicación se puedan comunicar entre sí. El proveedor de mensajes lo hemos creado como un servicio, para que toda la aplicación comparta la misma instancia.

El proveedor se basa en objetos Observable, concretamente del tipo Subject y más concretamente del tipo BehaviorSubject. Es un objeto de RxJS que nos permite tanto ordenarle que emita mensajes como suscribirnos para escucharlos. En definitiva, estamos usando programación reactiva para trabajar con un flujo asíncrono de datos: los mensajes.

Los componentes que emiten mensajes no tienen ni idea de quiénes se suscribirán a ellos, y los componentes que escuchan mensajes no tienen ni idea de quién los emite. Esto cumple con el principio de responsabilidad única, una de las reglas fundamentales de programación que debemos intentar seguir si queremos que nuestra aplicación sea sólida y escalable.

Por último, recordad siempre que las suscripciones a los mensajes hay que cancelarlas para evitar memory leaks.

Espero que os haya resultado útil y que podáis aplicarlo en vuestros desarrollos. El código completo del tutorial lo tenéis en GitHub. Si apreciáis algún error o queréis comentar algo, podéis dejar vuestra opinión un poco más abajo.

¿Qué opinas?

Tu dirección de correo electrónico no será publicada. * Campos obligatorios