El conocimiento es el nuevo dinero.
Aprender es la nueva manera en la que inviertes
Acceso Cursos

¿Cómo funcionan los eventos enviados por el servidor (SSE) o EventSource en Angular?

Caso de uso real con código y token de autorización

· 6 min de lectura
¿Cómo funcionan los eventos enviados por el servidor (SSE) o EventSource en Angular?

Me encontré con SSE a través de la historia de usuario de negocio de mi cliente (necesidad). Este es el caso de uso en el que lo utilicé. Imagina que necesitas añadir algunos datos a la base de datos. Digamos que usted tiene un archivo CSV que contiene cada línea de un artículo que usted necesita para empujar a la tabla de artículos de la base de datos. Sería mejor ver una barra de progreso en tiempo real que muestre cuánto ha procesado el servidor hasta el momento. ¿Verdad?

Entonces, en este artículo, voy a explicar:

¿Qué es SSE?
¿Por qué estoy usando SSE?
¿Cómo funciona?

¿Qué es SEE?


Server-Sent Events (SSE) es una tecnología server push que permite a un cliente recibir actualizaciones automáticas de un servidor a través de una conexión HTTP. La API Server-Sent Events EventSource está estandarizada como parte de HTML5 por el W3C. - Wikipedia

Así que Server-Sent-Events permite una conexión HTTP. Muy bien. Pero, no es realmente diferente de WebSocket, ¿verdad? Lo que es más importante es que permite a tu aplicación web recibir eventos llamados enviados por el servidor en un flujo unidireccional. Esto es lo que necesitaba en mi historia de usuario:

Hagamos una imagen mental para entender la diferencia. Voy a comparar SSE con WebSocket.

Esto es lo que ocurre en una conexión WebSocket frente a una conexión SSE:

En el caso de WebSocket, tenemos que llamar al servidor cada vez para obtener una respuesta. La comunicación es bidireccional. En cambio, en el caso de SSE, tenemos que preguntar por el porcentaje de progreso una vez y obtener varias respuestas cada vez indicándonos el porcentaje de progreso.

Recuerda que si tu quieres estar al tanto de las novedades de ANGULAR puedes visitar el siguiente video

B y C para la solución SSE


Por supuesto, siempre hay B y C para una solución. Para SSE, aquí están algunas de sus C's:

  1. Los navegadores Microsoft IE y Edge no son compatibles con SSE. Entonces necesitamos usar polyfills para hacer que SSE funcione en estos navegadores.
    La API EventSource no tiene soporte para cabeceras personalizadas.
    No hay soporte para datos binarios como en el caso de WebSocket.
  2. Todos los eventos en SSE están codificados en UTF-8.
  3. Dado que un navegador tiene un número limitado de conexiones por nombre de host, debemos prestar atención al número de flujos SSE abiertos al mismo tiempo.

¿Por qué elegí la ESS?


Principalmente tenemos 2 restricciones que nos hicieron elegir esta solución:

  1. Necesitamos visualizar un porcentaje de los datos procesados entre el contenido del fichero.
  2. Necesitamos recibir un detalle del registro sobre las líneas en las que tenemos errores con el tiempo.


Server-Sent Event

Es una buena solución para implementar este caso de uso. Primero, porque necesitamos mostrar el progreso en tiempo real. Segundo, porque se trata del progreso para la misma petición HTTP, no queremos enviar una petición HTTP cada vez para obtener el progreso y hacer lo mismo para los detalles del registro.

Lo que queremos es algo como lo siguiente:


SSE puede hacer eso, y por eso lo elegimos.

Por cierto, a veces también llamamos SSE por Source Event.

¿Cómo funciona?


Para permitir que los servidores envíen datos a las páginas web a través de HTTP o utilizando protocolos dedicados de envío desde el servidor (SSE), esta especificación introduce la interfaz EventSource.

En mi caso, utilizo keycloak para permitir SSO (single sign-on) para identificar a los usuarios y darles acceso. Por lo tanto, el EventSource estándar no me sirve, ya que necesito pasar el token para acceder al recurso que quiero.

Para resolver este problema, necesito instalar un paquete SSE que me permita pasar el token.

Estas son las pautas para usar SSE:

  1. Instalar SSE con NPM o Yarn
  2. Crea su Servicio de eventos de origen personalizado
  3. Crear el servicio ImportFile
  4. Ver SSE en acción

Si no necesitas un token, entonces puedes usar el EventSource estándar de HTML, sin necesidad de instalar paquetes extra. Pero, en aplicaciones reales, sin duda necesitamos un token por cuestiones de seguridad, ¿verdad?

Veamos un ejemplo de un caso real.

Implementación


1- Instalar SSE en la aplicación Angular


Para utilizar SSE, necesitamos instalarlo. En realidad hay muchos paquetes. Elegí un simple paquete js deseo es sse.js.

npm install sse.js

2- Crea tu Servicio de eventos de origen personalizado


Vamos a crear un SseService y el ImportService que necesita utilizar SSE para obtener el porcentaje de progreso.

Empecemos con nuestro SseService personalizado:

import { HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { SSE } from 'sse.js';
import { environment } from '$base/environments/environment';
import { KeycloakService } from '$service/keycloak.service';

@Injectable({
  providedIn: 'root',
})
/**
 * Single-Sent Event Service
 */
export class SseService {
  eventSource: SSE;
  /**
   * Constructor of SSE service
   * @param kcService injection to get the authorization token
   */
  constructor(private kcService: KeycloakService) {}

  /**
   * Create an event source of POST request
   * @param API url 
   * @formData data (file, ...etc.)
   */
  public getEventSourceWithPost(url: string, formData: FormData): SSE {
    return this.buildEventSource(url, 'POST', formData);
  }

   /**
   * Create an event source of GET request
   * @param API url 
   * @formData data (file, ...etc.)
   */
  public getEventSourceWithGet(url: string, formData: FormData): SSE {
    return this.buildEventSource(url, 'GET', formData);
  }

  /**
   * Building the event source
   * @param url  API URL
   * @param meth  (POST, GET, ...etc.)
   * @param formData data
   */
  private buildEventSource(url: string, meth: string, formData: FormData): SSE {
    const options = this.buildOptions(meth, formData);
    this.eventSource = new SSE(url, options);
    
    // add listener
     this.eventSource.addEventListener('message', (e) => {
      return e.data;
    });

    return  this.eventSource;
  }

  /**
   * close connection
   */
  public closeEventSource() {
    if (!! this.eventSource) {
       this.eventSource.close();
    }
  }

  /**
   * Récupération du token à passer dans le header de la requête
   */
  protected checkAuthorization(): string {
    const authToken = this.kcService.getToken() || '';
    // If we are on localhost (my environment.enableTokenInterceptor = false) we don't need a token
    const auth = environment.enableTokenInterceptor
      ? 'Bearer ' + authToken
      : '';
    return auth;
  }

  /**
   * Build query options
   * @param meth POST or GET
   * @param formData data
   */
  private buildOptions(
    meth: string,
    formData: FormData,
  ): {import { HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { SSE } from 'sse.js';
import { environment } from '$base/environments/environment';
import { KeycloakService } from '$service/keycloak.service';

@Injectable({
  providedIn: 'root',
})
/**
 * Single-Sent Event Service
 */
export class SseService {
  eventSource: SSE;
  /**
   * Constructor of SSE service
   * @param kcService injection to get the authorization token
   */
  constructor(private kcService: KeycloakService) {}

  /**
   * Create an event source of POST request
   * @param API url 
   * @formData data (file, ...etc.)
   */
  public getEventSourceWithPost(url: string, formData: FormData): SSE {
    return this.buildEventSource(url, 'POST', formData);
  }

   /**
   * Create an event source of GET request
   * @param API url 
   * @formData data (file, ...etc.)
   */
  public getEventSourceWithGet(url: string, formData: FormData): SSE {
    return this.buildEventSource(url, 'GET', formData);
  }

  /**
   * Building the event source
   * @param url  API URL
   * @param meth  (POST, GET, ...etc.)
   * @param formData data
   */
  private buildEventSource(url: string, meth: string, formData: FormData): SSE {
    const options = this.buildOptions(meth, formData);
    this.eventSource = new SSE(url, options);
    
    // add listener
     this.eventSource.addEventListener('message', (e) => {
      return e.data;
    });

    return  this.eventSource;
  }

  /**
   * close connection
   */
  public closeEventSource() {
    if (!! this.eventSource) {
       this.eventSource.close();
    }
  }

  /**
   * Récupération du token à passer dans le header de la requête
   */
  protected checkAuthorization(): string {
    const authToken = this.kcService.getToken() || '';
    // If we are on localhost (my environment.enableTokenInterceptor = false) we don't need a token
    const auth = environment.enableTokenInterceptor
      ? 'Bearer ' + authToken
      : '';
    return auth;
  }

  /**
   * Build query options
   * @param meth POST or GET
   * @param formData data
   */
  private buildOptions(
    meth: string,
    formData: FormData,
  ): {
    payload: FormData;
    method: string;
    headers: string | { Authorization: string };
  } {
    const auth = this.checkAuthorization();
    return {
      payload: formData,
      method: meth,
      headers: auth !== '' ? { Authorization: auth } : '',
    };
  }
}
    payload: FormData;
    method: string;
    headers: string | { Authorization: string };
  } {
    const auth = this.checkAuthorization();
    return {
      payload: formData,
      method: meth,
      headers: auth !== '' ? { Authorization: auth } : '',
    };
  }
}

3- Crear el servicio ImportFile

import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Injectable, NgZone } from '@angular/core';
import { SSE } from 'sse.js';
import { SseService } from './../sse/sse.service';

@Injectable({
  providedIn: 'root',
})

/*
 * Service to import files
 *
 */
export class ImportService {
  /**
   * Service constructor
   * @param zone to do some action outside the Angular zone
   * @param sseService inject the event source service
   */
  constructor(private zone: NgZone, private sseService: SseService) {}


   /**
   * Import file
   * @param file to import
   * @return obervable that has the progress
   */
  public importFile(file: File): Observable<number> {
    const formData = new FormData();
    formData.append('file', file);
    // you can append other data if needed
    // formData.append('param', variable);

    return this.getServerSentEvent(API_URL, formData);
  }

  /**
   * Get event source (SSE)
   */
  private getServerSentEvent(url: string, data: FormData): Observable<number> {
    return Observable.create((observer) => {
      const eventSource = this.sseService.getEventSourceWithPost(url, data);
      // Launch query
      eventSource.stream();
      // on answer from message listener 
      eventSource.onmessage = (event) => {
        this.zone.run(() => {
          observer.next(event.progress);
        });
      };
      eventSource.onerror = (error) => {
        this.zone.run(() => {
          observer.error(error);
        });
      };
    });
  }


  protected closeConnection(eventSource: SSE): void {
    this.sseService.closeEventSource(eventSource);
  }
}

4- La ESS en acción


@Component({
  selector: 'example',
  templateUrl: './example.component.html',
  styleUrls: ['./example.component.scss'],
})
export class ExampleComponent implements OnInit {
  
/**
 * Import File handler
 */
public confirmImport(): void {
    this.importService.importFile(this.currentFile).subscribe(
      (progress) => {
        this.importProgress = progress;
      },
      (error) => {
        this.logger.error(
          `Error while getting file import progress: ${error}`,
        );
      },
      () => {
        // on complete, close connection 
        this.importService.closeConnetion();
      },
    );
  }
  
  
}

Como ves este código es sólo para el lado del cliente. Todavía necesitamos implementar el back-side o el servidor que recibirá nuestra llamada preguntando por el progreso.

Aquí hay diferentes implementaciones basadas en su tecnología de backend. He enumerado las tecnologías más utilizadas ejemplo en estas referencias :

  1. Para usuarios de Java y Spring: link.
  2. Para usuarios de Express backend y NodeJS: link.

Queridos lectores y amigos, gracias por vuestro apoyo y vuestro precioso tiempo. Espero que os haya sido útil y provechoso.

Fuente

Plataforma de cursos gratis sobre programación