Salta al contenuto principale

Come gestire le code di richieste HTTP in Angular

Profile picture for user luca77king

Le richieste HTTP sono uno dei concetti fondamentali per le applicazioni web moderne. Quando un'applicazione web deve ottenere o inviare dati, di solito lo fa attraverso una richiesta HTTP. Tuttavia, la gestione di queste richieste può essere un compito impegnativo, soprattutto quando si tratta di applicazioni che richiedono molte chiamate API contemporaneamente.

Per gestire efficacemente le richieste HTTP in Angular, è importante capire come funzionano le code (queque) di richieste. Una coda di richieste HTTP è essenzialmente una struttura dati che gestisce le richieste in arrivo in modo da non sovraccaricare il server con troppe richieste contemporaneamente. Quando si utilizzano le code di richieste, le richieste vengono messe in attesa in una coda e inviate una alla volta, in modo che il server possa gestirle in modo efficiente.

La gestione delle code di richieste HTTP in Angular è importante perché può aiutare a migliorare le prestazioni dell'applicazione, evitare errori e garantire un'esperienza utente ottimale. Quando le richieste HTTP non vengono gestite correttamente, l'applicazione può diventare lenta e poco reattiva, causando frustrazione agli utenti e possibili problemi di sicurezza. Inoltre, una gestione inefficiente delle richieste HTTP può portare a errori server, che possono interrompere completamente l'applicazione. Per questo motivo, è importante gestire le code di richieste HTTP in modo efficace.

Utilizzo dei servizi per la gestione delle richieste HTTP

In Angular, i servizi sono una delle principali tecniche utilizzate per organizzare il codice e separare la logica di business dal resto dell'applicazione.

Nel contesto della gestione delle richieste HTTP, i servizi sono utilizzati per creare una singola istanza e gestire le richieste in modo centralizzato. Ciò consente di evitare la duplicazione del codice e semplifica la manutenzione dell'applicazione.

Ad esempio, supponiamo di avere un componente che deve effettuare una chiamata API per recuperare i dati da visualizzare all'interno dell'applicazione. Invece di scrivere la logica per la chiamata API all'interno del componente, possiamo creare un servizio dedicato che gestirà la chiamata API e restituirà i dati al componente.

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class DataService {

  private apiUrl = 'https://api.example.com/data';

  constructor(private http: HttpClient) { }

  getData(): Observable<any> {
    return this.http.get<any>(this.apiUrl);
  }
}

In questo esempio, abbiamo creato un servizio DataService che utilizza il servizio HttpClient di Angular per effettuare una chiamata GET all'API https://api.example.com/data. Il servizio restituirà un'istanza di Observable che il componente potrà sottoscrivere per ottenere i dati.

Per utilizzare il servizio nel componente, possiamo iniettarlo nel costruttore del componente e chiamare il metodo getData del servizio:

import { Component, OnInit } from '@angular/core';
import { DataService } from '../data.service';

@Component({
  selector: 'app-data',
  templateUrl: './data.component.html',
  styleUrls: ['./data.component.css']
})
export class DataComponent implements OnInit {

  data: any;

  constructor(private dataService: DataService) { }

  ngOnInit(): void {
    this.dataService.getData().subscribe(
      response => {
        this.data = response;
      },
      error => {
        console.error(error);
      }
    );
  }

}

Fin qui tutto bene, abbiamo visto come utilizzare i servizi per gestire le richieste HTTP in modo centralizzato e semplificare la manutenzione dell'applicazione. Tuttavia, se il componente gestisce più di una richiesta HTTP contemporaneamente, potrebbero verificarsi problemi di sovraccarico del server e di accavallamento delle richieste.

Creazione di una coda per le richieste HTTP

La tecnica che ho adottato io per gestire le richieste HTTP in modo centralizzato e semplificare la manutenzione dell'applicazione è quella di creare un servizio padre che gestirà tutte le richieste HTTP dei servizi figli.

Passiamo direttamente all'azione, creando un nuovo servizio che chiameremo BaseService

import {HttpClient} from '@angular/common/http';
import {Observable, of, Subject, Subscription} from 'rxjs';

export class BaseService {

  private queue: PendingRequest[] = [];

  protected constructor(protected http: HttpClient) {}
}

Questa è la struttura base del servizio base, non usiamo il decoratore @Injectable perchè questa classe non verrà iniettata nei componenti, ma lo saranno i servizi che la estendono.

Ora creiamo un'altra classe, PendingRequest. Per praticità la inserisco all'interno dello stesso file.

import {HttpClient} from '@angular/common/http';
import {Observable, of, Subject, Subscription} from 'rxjs';

class PendingRequest {
  url: string;
  method: string;
  options?: any;
  body?: any;
  subscription: Subject<any>;

  constructor(url: string, method: string, options: any | null, body: any | null, subscription: Subject<any>) {
    this.url = url;
    this.method = method;
    this.options = options;
    this.body = body;
    this.subscription = subscription;
  }
}

export class BaseService {

  private queue: PendingRequest[] = [];

  protected constructor(protected http: HttpClient) {}
}

La classe PendingRequest è una classe di supporto che rappresenta una singola richiesta da effettuare.

Ha le seguenti proprietà:

  • url: la URL della richiesta
  • method: il metodo HTTP da utilizzare per la richiesta (ad esempio, "get" o "post")
  • options: eventuali opzioni da passare alla richiesta (ad esempio, intestazioni aggiuntive)
  • body: il corpo della richiesta (usato solo nel caso di una richiesta POST)
  • subscription: un oggetto Subject che viene utilizzato per risolvere o rigettare l'Observable associato alla richiesta

La classe ha anche un costruttore che prende in input tutti i parametri necessari e li assegna alle relative proprietà dell'istanza.

La classe PendingRequest viene utilizzata dalla classe BaseService per rappresentare le richieste da effettuare e per tenere traccia dei relativi Observable. Quando una richiesta viene aggiunta alla coda, viene creata un'istanza di PendingRequest che rappresenta la richiesta e che contiene un riferimento all'Observable associato. Questo Observable viene restituito al chiamante del metodo get o post, in modo che possa essere utilizzato per ricevere la risposta della richiesta.

Quando una richiesta viene completata, il relativo Observable viene risolto o rigettato tramite il Subject associato, in modo che il chiamante possa ricevere la risposta o l'errore.

Ora definiamo due nuovi metodi all'interno del nostro BaseService

  private startNextRequest() {
    if (this.queue.length > 0) {
      this.execute(this.queue[0]);
    }
  }

  private execute<T>(requestData: PendingRequest): Subscription | Observable<T> {
    if(requestData.method === 'get') {
      return this.http.get<T>(requestData.url, requestData.options)
        .subscribe(res => {
          const sub = requestData.subscription;
          sub.next(res);
          this.queue.shift();
          this.startNextRequest();
        });
    }
    return this.http.post<T>(requestData.url, requestData.body, requestData.options).subscribe(res => {
        const sub = requestData.subscription;
        sub.next(res);
        this.queue.shift();
        this.startNextRequest();
      }
    );
  }

Il primo metodo, startNextRequest(), controlla se c'è almeno una richiesta in attesa nella coda, e in tal caso esegue la esegue chiamando il secondo metodo, execute(). Quest'ultimo riceve un oggetto PendingRequest come argomento, che rappresenta la richiesta HTTP in sospeso. La funzione esegue la richiesta tramite il metodo get() o post() del modulo HttpClient di Angular, a seconda del valore method dell'oggetto PendingRequest. Se la richiesta viene eseguita correttamente, il metodo execute() notifica il risultato alla sottoscrizione associata alla richiesta, rimuove la richiesta dalla coda, e avvia la prossima richiesta in attesa chiamando di nuovo il metodo startNextRequest().

Completiamo la classe inserendo gli ultimi tre metodi

  protected get<T>(url: string, options: any | null) : Observable<T>
  {
    return this.addRequestToQueue(url, 'get', options);
  }

  protected post<T>(url: string, body: any| null, options: any | null)
  {
    return this.addRequestToQueue(url, 'post', options, body);
  }

  private addRequestToQueue(url: string, method: string, options: any | null, body?: any) {
    const subscription = new Subject<any>();
    const request = new PendingRequest(url, method, options, body, subscription);
    this.queue.push(request);
    if (this.queue.length === 1) {
      this.startNextRequest();
    }
    return subscription;
  }

Il primo metodo, get<T>(), è un metodo pubblico che accetta un URL e un oggetto options opzionale come argomenti e restituisce un oggetto Observable<T>. Questo metodo utilizza il metodo privato addRequestToQueue() per aggiungere una nuova richiesta HTTP di tipo "get" alla coda e restituire l'oggetto Subject associato alla nuova richiesta.

Il secondo metodo, post<T>(), è simile al metodo get<T>(), ma accetta un parametro aggiuntivo body che rappresenta il corpo della richiesta HTTP. Questo metodo utilizza anche il metodo privato addRequestToQueue() per aggiungere una nuova richiesta HTTP di tipo "post" alla coda e restituire l'oggetto Subject associato alla nuova richiesta.

Infine, il metodo privato addRequestToQueue() è utilizzato dai metodi get<T>() e post<T>() per creare una nuova istanza dell'oggetto PendingRequest, aggiungerla alla coda e restituire l'oggetto Subject associato alla nuova richiesta. Il metodo addRequestToQueue() controlla inoltre se la coda è vuota e, in tal caso, avvia immediatamente la richiesta chiamando il metodo startNextRequest().

Come utilizzare il BaseService

È necessario creare una nuova classe che estenda il nostro BaseService. La nuova classe deve includere tutte le funzionalità del servizio originale e può aggiungere funzionalità aggiuntive se necessario.

Ad esempio, supponiamo di voler creare un servizio che gestisca le richieste HTTP per l'API di un negozio online. Possiamo creare una nuova classe ShopService che estende la classe BaseService e fornire i metodi specifici per l'API del nostro negozio:

import { Injectable } from '@angular/core';
import { BaseService } from './base.service';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class ShopService extends BaseService {

  constructor(protected override http: HttpClient) {
    super(http);
  }

  // Esempio di metodo get per l'API del negozio
  public getProducts(): Observable<Product[]> {
    return this.get<Product[]>('https://example.com/api/products', null);
  }

  // Esempio di metodo post per l'API del negozio
  public createOrder(order: Order): Observable<Order> {
    return this.post<Order>('https://example.com/api/orders', order, null);
  }
}

In questo esempio, la classe ShopService estende la classe BaseService e definisce due metodi aggiuntivi per gestire le richieste HTTP per l'API del negozio: getProducts() e createOrder(). Questi metodi utilizzano i metodi get<T>() e post<T>() ereditati dalla classe HttpQueueService per effettuare le richieste HTTP.