Los patrones de comunicación en JavaScript se refieren a enfoques o prácticas comunes para establecer la comunicación entre diferentes partes de una aplicación, ya sea entre componentes, módulos, objetos o sistemas distribuidos.
Estos patrones facilitan la organización del código, la gestión de eventos, la transferencia de datos y la interacción entre diferentes partes de una aplicación. Los patrones de comunicación entre módulos o componentes son fundamentales en el diseño de sistemas de software, ya que determinan cómo interactúan entre sí las distintas partes de una aplicación.
En Angular, el patrón de Publicador-Suscriptor (Observer) se utiliza en varios lugares, y uno de los ejemplos más notables es en el sistema de manejo de eventos mediante el uso de servicios y el patrón de diseño de Observables. Angular utiliza RxJS (Reactive Extensions for JavaScript) para implementar el patrón Observer. Si se desea comunicar cambios desde un componente A a otro componente B. Se puede utilizar un servicio como intermediario y emitir eventos desde el componente A que el componente B puede escuchar.
Crear un servicio para manejar la comunicación
import { Injectable } from '@angular/core'; import { Subject } from 'rxjs'; @Injectable({ providedIn: 'root', }) export class ComunicacionService { private subject = new Subject<any>(); enviarMensaje(mensaje: any) { this.subject.next({ mensaje }); } recibirMensaje() { return this.subject.asObservable(); } }
En el componente emisor
import { Component } from '@angular/core'; import { ComunicacionService } from 'ruta-al-servicio'; @Component({ selector: 'app-componente-a', template: ` <button (click)="enviar()">Enviar Mensaje</button> `, }) export class ComponenteA { constructor(private comunicacionService: ComunicacionService) {} enviar() { this.comunicacionService.enviarMensaje('Hola desde Componente A'); } }
En el componente receptor
import { Component, OnDestroy } from '@angular/core'; import { ComunicacionService } from 'ruta-al-servicio'; import { Subscription } from 'rxjs'; @Component({ selector: 'app-componente-b', template: ` <p>Mensaje recibido: {{ mensajeRecibido }}</p> `, }) export class ComponenteB implements OnDestroy { mensajeRecibido: any; private subscription: Subscription; constructor(private comunicacionService: ComunicacionService) { this.subscription = this.comunicacionService.recibirMensaje().subscribe((mensaje) => { this.mensajeRecibido = mensaje; }); } ngOnDestroy() { this.subscription.unsubscribe(); } }
El patrón de Invocación Remota (Remote Procedure Call - RPC) es una técnica que permite la ejecución de funciones o métodos en un sistema remoto como si fueran locales. En el contexto de JavaScript, existen varias implementaciones y tecnologías que permiten realizar llamadas remotas.
Pasos básicos para implementar un gRPC con node;
1- Define un archivo .proto para describir el servicio:
syntax = "proto3"; service Greeter { rpc SayHello (HelloRequest) returns (HelloReply); } message HelloRequest { string name = 1; } message HelloReply { string message = 1; }
2- Compilar archivo .proto
Utiliza el compilador de protobuf para generar los archivos JavaScript y TypeScript necesarios:
protoc --proto_path=. --js_out=import_style=commonjs,binary:./generated --grpc_out=./generated --plugin=protoc-gen-grpc=`which grpc_tools_node_protoc_plugin` example.proto
Esto generará los archivos example_grpc_pb.js y example_pb.js en el directorio generated.
3- Implementar el Servidor gRPC
Crea un archivo server.js para implementar el servidor:
const grpc = require('grpc'); const { GreeterService } = require('./generated/example_grpc_pb'); const { HelloReply, HelloRequest } = require('./generated/example_pb'); const server = new grpc.Server(); server.addService(GreeterService, { sayHello: (call, callback) => { const request = call.request; const reply = new HelloReply(); reply.setMessage(`Hello, ${request.getName()}!`); callback(null, reply); }, }); const PORT = 50051; server.bind(`0.0.0.0:${PORT}`, grpc.ServerCredentials.createInsecure()); console.log(`Server running at http://0.0.0.0:${PORT}`); server.start();
4- Implementar el Cliente gRPC
Crea un archivo client.js para implementar el cliente:
const grpc = require('grpc'); const { GreeterClient } = require('./generated/example_grpc_pb'); const { HelloRequest } = require('./generated/example_pb'); const client = new GreeterClient('localhost:50051', grpc.credentials.createInsecure()); const request = new HelloRequest(); request.setName('John Doe'); client.sayHello(request, (error, response) => { if (!error) { console.log(`Greeting: ${response.getMessage()}`); } else { console.error(error); } });
5- Ejecutar el Servidor y el Cliente
Inicia el servidor ejecutando node server.js y luego ejecuta el cliente con node client.js
1- Configurar Servidor gRPC en node:
Antes de integrar gRPC en tu aplicación React, asegúrate de tener un servidor gRPC en funcionamiento. Puedes seguir los pasos anteriores para configurar un servidor gRPC en Node.js.
2- Configurar el Cliente gRPC en React
Instala las dependencias necesarias para el cliente gRPC en tu aplicación React
npm install grpc-web
3- Crear un Cliente gRPC en React
Se puede crear un cliente gRPC en react en un component o un servicio
import React, { useState } from 'react'; import { GreeterClient } from './generated/example_pb_service'; import { HelloRequest } from './generated/example_pb'; const GrpcExample = () => { const [response, setResponse] = useState(''); const handleGrpcRequest = async () => { const request = new HelloRequest(); request.setName('John Doe'); const client = new GreeterClient('http://localhost:8080'); client.sayHello(request, {}, (err, response) => { if (!err) { setResponse(response.getMessage()); } else { console.error(err); } }); }; return ( <div> <h1>gRPC Example in React</h1> <button onClick={handleGrpcRequest}>Make gRPC Request</button> <p>Response: {response}</p> </div> ); }; export default GrpcExample;
4- Configurar el Proxy para gRPC-Web
Como gRPC-Web no es compatible directamente con el navegador, necesitarás configurar un proxy para redirigir las solicitudes gRPC-Web al servidor gRPC. Puedes utilizar grpcwebproxy para esto
Instala e inicia grpcwebproxy y ejecuta la aplicación react
go get github.com/improbable-eng/grpc-web/go/grpcwebproxy grpcwebproxy --backend_addr=localhost:50051 --run_tls_server=false --allow_all_origins npm start
El patrón de Cola de Mensajes (Message Queue) es un patrón de comunicación entre componentes o sistemas que implica el envío y recepción de mensajes a través de una cola compartida. Este patrón es útil para desacoplar componentes, permitiéndoles comunicarse de manera asíncrona sin depender directamente unos de otros. Varios frameworks y bibliotecas de JavaScript proporcionan implementaciones y soporte para el patrón de Cola de Mensajes.
Implementar el patrón de Cola de Mensajes en Node.js puede realizarse utilizando diversas bibliotecas y servicios. Una práctica común es utilizando RabbitMQ y la biblioteca amqplib, que es mayormente utilizada en entornos Node.js para interactuar con RabbitMQ. Asegúrate de tener RabbitMQ instalado y en ejecución localmente.
1- Instalacion de amqplib
npm install amqplib
2- Definir emisor
const amqp = require('amqplib'); async function enviarMensaje() { const connection = await amqp.connect('amqp://localhost'); const channel = await connection.createChannel(); const cola = 'mi_cola'; const mensaje = 'Hola, este es un mensaje en la cola'; await channel.assertQueue(cola, { durable: false }); channel.sendToQueue(cola, Buffer.from(mensaje)); console.log(`Mensaje enviado: ${mensaje}`); setTimeout(() => { connection.close(); }, 500); } enviarMensaje();
3- consumidor
const amqp = require('amqplib'); async function recibirMensaje() { const connection = await amqp.connect('amqp://localhost'); const channel = await connection.createChannel(); const cola = 'mi_cola'; await channel.assertQueue(cola, { durable: false }); console.log(`Esperando mensajes en la cola ${cola}`); channel.consume(cola, (mensaje) => { console.log(`Mensaje recibido: ${mensaje.content.toString()}`); }, { noAck: true }); } recibirMensaje();
Estos ejemplos son simples y utilizan RabbitMQ como backend, pero el patrón de Cola de Mensajes se puede implementar con otras soluciones, como Apache Kafka, Redis o servicios en la nube como AWS SQS o Google Cloud Pub/Sub, según los requisitos específicos de la aplicación.
En Angular, la implementación de un sistema de Message Queue (Cola de Mensajes) generalmente se lleva a cabo utilizando servicios y observables para facilitar la comunicación entre componentes de manera desacoplada. Aunque Angular no tiene una implementación nativa de colas de mensajes como en algunos sistemas de backend, puedes lograr un patrón similar usando los servicios y observables proporcionados por Angular.
Pasos para emular un patron de mensajes en cola
1- Crear un servicio de Message Queue
import { Injectable } from '@angular/core'; import { Subject, Observable } from 'rxjs'; @Injectable({ providedIn: 'root', }) export class MessageQueueService { private messageSubject = new Subject<any>(); sendMessage(message: any) { this.messageSubject.next(message); } getMessage(): Observable<any> { return this.messageSubject.asObservable(); } }
2- Productor (Componente Emisor)
import { Component } from '@angular/core'; import { MessageQueueService } from './message-queue.service'; @Component({ selector: 'app-productor', template: ` <button (click)="enviarMensaje()">Enviar Mensaje</button> `, }) export class ProductorComponent { constructor(private messageQueueService: MessageQueueService) {} enviarMensaje() { const mensaje = 'Hola desde el componente productor'; this.messageQueueService.sendMessage(mensaje); } }
3- Consumidor (Componente Receptor)
import { Component, OnDestroy } from '@angular/core'; import { MessageQueueService } from './message-queue.service'; import { Subscription } from 'rxjs'; @Component({ selector: 'app-consumidor', template: ` <p>Mensaje recibido: {{ mensajeRecibido }}</p> `, }) export class ConsumidorComponent implements OnDestroy { mensajeRecibido: any; private subscription: Subscription; constructor(private messageQueueService: MessageQueueService) { this.subscription = this.messageQueueService.getMessage().subscribe((mensaje) => { this.mensajeRecibido = mensaje; }); } ngOnDestroy() { this.subscription.unsubscribe(); } }
4- Configurar Módulo de Angular
import { NgModule } from '@angular/core'; import { ProductorComponent } from './productor.component'; import { ConsumidorComponent } from './consumidor.component'; import { MessageQueueService } from './message-queue.service'; @NgModule({ declarations: [ProductorComponent, ConsumidorComponent], providers: [MessageQueueService], }) export class AppModule {}
Con este enfoque, el servicio MessageQueueService actúa como una cola de mensajes. El componente emisor (ProductorComponent) utiliza el servicio para enviar mensajes, y el componente receptor (ConsumidorComponent) se suscribe al servicio para recibir y procesar esos mensajes.
El patrón de Paso de Mensajes (Message Passing) es un enfoque en el que los distintos componentes de un sistema se comunican enviándose mensajes. Este patrón es comúnmente utilizado en sistemas basados en eventos, donde los componentes interactúan de manera asincrónica y desacoplada. En el contexto de JavaScript, especialmente en frameworks y bibliotecas, este patrón se implementa a menudo mediante el uso de eventos, observables o técnicas similares.
1- Crear un servicio de Mensajes:
// message.service.ts import { Injectable } from '@angular/core'; import { Subject, Observable } from 'rxjs'; @Injectable({ providedIn: 'root', }) export class MessageService { private messageSubject = new Subject<any>(); sendMessage(message: any) { this.messageSubject.next(message); } getMessage(): Observable<any> { return this.messageSubject.asObservable(); } }
2- Componente Emisor (Productor):
// producer.component.ts import { Component } from '@angular/core'; import { MessageService } from './message.service'; @Component({ selector: 'app-producer', template: ` <button (click)="sendMessage()">Enviar Mensaje</button> `, }) export class ProducerComponent { constructor(private messageService: MessageService) {} sendMessage() { const message = 'Hola desde el componente emisor'; this.messageService.sendMessage(message); } }
3- Componente Receptor (Consumidor):
// consumer.component.ts import { Component, OnDestroy } from '@angular/core'; import { MessageService } from './message.service'; import { Subscription } from 'rxjs'; @Component({ selector: 'app-consumer', template: ` <p>Mensaje recibido: {{ receivedMessage }}</p> `, }) export class ConsumerComponent implements OnDestroy { receivedMessage: any; private subscription: Subscription; constructor(private messageService: MessageService) { this.subscription = this.messageService.getMessage().subscribe((message) => { this.receivedMessage = message; }); } ngOnDestroy() { this.subscription.unsubscribe(); } }
4- Configurar el Módulo Angular:
// app.module.ts import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { ProducerComponent } from './producer.component'; import { ConsumerComponent } from './consumer.component'; import { MessageService } from './message.service'; @NgModule({ imports: [BrowserModule], declarations: [ProducerComponent, ConsumerComponent], providers: [MessageService], bootstrap: [ProducerComponent, ConsumerComponent], }) export class AppModule {}
El servicio MessageService actúa como un canal de mensajes. El componente emisor (ProducerComponent) utiliza el servicio para enviar mensajes, y el componente receptor (ConsumerComponent) se suscribe al servicio para recibir y procesar esos mensajes.
En React, el patrón de paso de mensajes se implementa típicamente a través de la propagación de props y el uso de funciones de devolución de llamada (callbacks) o, en casos más avanzados, mediante la utilización de contextos o la gestión de estados con estados globales (Redux, Recoil, etc.).
Paso de mensajes mediante Props:
1- Componente Emisor (Productor):
import React from 'react'; const Productor = ({ enviarMensaje }) => ( <button onClick={() => enviarMensaje('Hola desde el componente emisor')}>Enviar Mensaje</button> );
2- Componente Receptor (Consumidor):
import React from 'react'; const Consumidor = ({ mensajeRecibido }) => ( <p>Mensaje recibido: {mensajeRecibido}</p> );
3- Componente Principal:
import React, { useState } from 'react'; import Productor from './Productor'; import Consumidor from './Consumidor'; const App = () => { const [mensaje, setMensaje] = useState(''); const recibirMensaje = (mensaje) => { setMensaje(mensaje); }; return ( <div> <Productor enviarMensaje={recibirMensaje} /> <Consumidor mensajeRecibido={mensaje} /> </div> ); }; export default App;
El patrón de middleware es comúnmente utilizado en muchos frameworks y librerías de JavaScript para gestionar el flujo de ejecución y realizar tareas intermedias en el procesamiento de solicitudes o eventos.
En Express.js, un middleware es una función que tiene acceso a los objetos de solicitud (req), respuesta (res), y la siguiente función en la cadena de middleware (next). Puedes utilizar múltiples middlewares para procesar una solicitud en orden.
const express = require('express'); const app = express(); // Middleware const loggerMiddleware = (req, res, next) => { console.log(`Solicitud recibida: ${req.method} ${req.url}`); next(); // Llama a la siguiente función de middleware en la cadena }; // Usa el middleware para todas las rutas app.use(loggerMiddleware); // Ruta principal app.get('/', (req, res) => { res.send('Hola, mundo!'); }); app.listen(3000, () => { console.log('Servidor escuchando en el puerto 3000'); });
Koa.js es un framework web más moderno construido sobre las lecciones aprendidas de Express.js. Utiliza middlewares de una manera más eficiente y utiliza funciones asíncronas.
const Koa = require('koa'); const app = new Koa(); // Middleware const loggerMiddleware = async (ctx, next) => { console.log(`Solicitud recibida: ${ctx.method} ${ctx.url}`); await next(); // Llama a la siguiente función de middleware en la cadena }; // Usa el middleware app.use(loggerMiddleware); // Ruta principal app.use(async (ctx) => { ctx.body = 'Hola, mundo!'; }); app.listen(3000, () => { console.log('Servidor escuchando en el puerto 3000'); });
En el contexto de React y Redux, los middlewares son funciones que se intercalan entre la acción que se despacha y el momento en que llega al reducer. Permiten realizar tareas adicionales, como manejar acciones asíncronas, realizar logs, o incluso modificar la acción antes de que alcance al reducer.
Un middleware en Redux sigue el siguiente formato:
const miMiddleware = (store) => (next) => (action) => { // Código del middleware next(action); // Llama al siguiente middleware o al reducer };
En React y Redux, uno de los middlewares más comúnmente utilizado es redux-thunk. Este middleware permite manejar acciones asíncronas y devuelve funciones desde los creadores de acciones en lugar de objetos simples. A continuación, un ejemplo básico de cómo usar redux-thunk:
Instalación de Redux Thunk
npm install redux-thunk
Configuración de redux-thunk
// store.js import { createStore, applyMiddleware } from 'redux'; import thunk from 'redux-thunk'; import rootReducer from './reducers'; // Importa tu rootReducer aquí const store = createStore( rootReducer, applyMiddleware(thunk) ); export default store;
Uso de redux-thunk
// actions.js const fetchDataStart = () => ({ type: 'FETCH_DATA_START', }); const fetchDataSuccess = (data) => ({ type: 'FETCH_DATA_SUCCESS', payload: data, }); const fetchDataError = (error) => ({ type: 'FETCH_DATA_ERROR', payload: error, }); // El siguiente creador de acciones devuelve una función gracias a redux-thunk const fetchData = () => { return async (dispatch) => { dispatch(fetchDataStart()); try { const response = await fetch('https://api.example.com/data'); const data = await response.json(); dispatch(fetchDataSuccess(data)); } catch (error) { dispatch(fetchDataError(error)); } }; }; export { fetchData };
En este ejemplo, fetchData es un creador de acciones que, gracias a redux-thunk, puede devolver una función en lugar de un objeto de acción. Esta función puede realizar operaciones asíncronas y despachar otras acciones en diferentes momentos del proceso.
El patrón de Inyección de Dependencias (DI, por sus siglas en inglés Dependency Injection) es una técnica en la que un objeto recibe sus dependencias de una fuente externa en lugar de crearlas internamente. Este patrón es comúnmente utilizado en frameworks y librerías de programación para mejorar la modularidad y la prueba unitaria, y es fundamental en el desarrollo de aplicaciones basadas en inversion de control (IoC).
DI en Angular
En Angular, la Inyección de Dependencias es un concepto central. Angular tiene su propio sistema de Inyección de Dependencias que permite la creación y gestión de instancias de servicios.
import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root', }) export class MiServicio { // Lógica del servicio }
En este ejemplo, @Injectable marca la clase como un servicio que Angular puede inyectar automáticamente cuando sea necesario. El uso de providedIn: 'root' significa que este servicio es singleton y está disponible en toda la aplicación.
En Node.js, la Inyección de Dependencias puede ser realizada manualmente o utilizando bibliotecas como InversifyJS o Awilix.
class MiServicio { // Lógica del servicio } class MiComponente { constructor(miServicio) { this.miServicio = miServicio; } // Lógica del componente usando this.miServicio } const miServicio = new MiServicio(); const miComponente = new MiComponente(miServicio);
En este ejemplo, MiComponente recibe MiServicio como una dependencia que debe ser proporcionada externamente.
Patrón de Puente (Bridge):
El patrón de Puente (Bridge) es un patrón estructural que separa una abstracción de su implementación, permitiendo que ambas evolucionen de manera independiente. Este patrón es útil cuando tienes varias dimensiones de variación y deseas evitar una explosión combinatoria de clases, proporcionando una interfaz más flexible.
En el contexto de los frameworks JavaScript, el patrón de Puente puede aplicarse para desacoplar componentes y facilitar la extensión y la variación de funcionalidades.
// Implementación de la interfaz (Implementor) class DibujoAPI { dibujarCirculo(x, y, radio) { console.log(`Dibujando un círculo en (${x}, ${y}) con radio ${radio}`); } } // Abstracción (Abstraction) class Forma { constructor(dibujoAPI) { this.dibujoAPI = dibujoAPI; } dibujar() { // Implementación común } redimensionar(factor) { // Implementación común } } // Abstracción refinada (Refined Abstraction) class Circulo extends Forma { constructor(x, y, radio, dibujoAPI) { super(dibujoAPI); this.x = x; this.y = y; this.radio = radio; } dibujar() { this.dibujoAPI.dibujarCirculo(this.x, this.y, this.radio); } redimensionar(factor) { this.radio *= factor; } } // Uso del patrón de Puente const dibujoAPIV1 = new DibujoAPI(); const circuloV1 = new Circulo(1, 2, 3, dibujoAPIV1); circuloV1.dibujar(); // Dibujando un círculo en (1, 2) con radio 3 circuloV1.redimensionar(2); circuloV1.dibujar(); // Dibujando un círculo en (1, 2) con radio 6 // Puedes introducir una nueva implementación sin modificar las clases existentes class DibujoAPIV2 { dibujarCirculo(x, y, radio) { console.log(`Nueva implementación: Dibujando un círculo en (${x}, ${y}) con radio ${radio}`); } } const dibujoAPIV2 = new DibujoAPIV2(); const circuloV2 = new Circulo(4, 5, 6, dibujoAPIV2); circuloV2.dibujar(); // Nueva implementación: Dibujando un círculo en (4, 5) con radio 6
En este ejemplo:
Esta es una guía completa de los patrones de comunicación en JavaScript.