I servizi in Angular | Aulab
TECH SUMMER LAB 🚀 Open Month
| 6 lezioni gratuite per orientarti al meglio e iniziare a costruire il tuo futuro digitale! Iscriviti gratis

GUIDE PER ASPIRANTI PROGRAMMATORI

I servizi in Angular

Fino a questo momento, in tutti gli esempi che abbiamo fatto, le funzionalità erano contenute completamente nella porzione di codice esemplificata. In casi più concreti, la complessità generale del progetto richiede che molte funzionalità siano trasversali a più componenti, oppure che ci siano dei contesti di dati o funzionalità accessibili più o meno globalmente, senza…

Lezione 11 / 17
Enza Neri
Immagine di copertina

Vuoi avviare una nuova carriera o fare un upgrade?

Trova il corso Digital & Tech più adatto a te nel nostro catalogo!

Fino a questo momento, in tutti gli esempi che abbiamo fatto, le funzionalità erano contenute completamente nella porzione di codice esemplificata.

In casi più concreti, la complessità generale del progetto richiede che molte funzionalità siano trasversali a più componenti, oppure che ci siano dei contesti di dati o funzionalità accessibili più o meno globalmente, senza dover esplicitamente trasmettere tutto tramite i binding, che rendono più rigidi e verbosi i template.

Tipicamente nelle architetture OOP si incapsulano dati e funzioni trasversali all’interno di classi servizio, e le istanze di queste classi servizio sono condivise secondo determinate logiche, tramite un sistema di iniezione delle dipendenze (dependency injection, o DI).

Angular fornisce nativamente un sistema di DI con il quale una classe, sia essa un componente, una direttiva o altro, può richiedere un’istanza di un certo servizio.

Andiamo, dunque, subito a vedere come implementare e utilizzare una service class.


Riprendendo l’esempio del contatore, vediamo come possiamo utilizzare dei servizi per rendere più flessibile e potente questa funzionalità.

Counter service in Angular

Per prima cosa, vogliamo spostare le logiche del nostro contatore, dalla classe componente alla classe servizio:

ng generate service counter/counter

In questo modo il nostro counter.service.ts viene creato direttamente nella cartella del counter component. Spostando il codice, il nostro counter.service.ts diventa:

import { Injectable } from '@angular/core';


@Injectable({ providedIn: 'root' })
export class CounterService {
  count = 0;


  increment() { this.count++ }
  reset() { this.count = 0 }
}

Fin qui, niente di sorprendente; il decoratore @Injectable farà in modo che CounterService sia disponibile a chi ne chieda un’istanza; la cosa bella è che se CounterService dovesse avere a sua volta degli injectable da cui dipende, questi verrebbero tutti forniti in automatico.

Vediamo come cambia il codice del nostro counter.component.ts:

import { Component } from '@angular/core';
import { CounterService } from './counter.service';


@Component({
  selector: 'app-counter',
  standalone: true,
  imports: [],
  templateUrl: './counter.component.html',
  styleUrl: './counter.component.css'
})
export class CounterComponent {
  constructor(protected counter: CounterService) { }
}

La nostra classe componente ora non fa altro che richiedere come argomento al costruttore un’istanza di CounterService. Angular prenderà nota di questa dipendenza e farà in modo che sia soddisfatta. L’accessibilità di counter è protected in modo tale che sia accessibile dal template (anche public andrebbe bene, ma non private).

Vediamo counter.component.html:

<h2>Counter</h2>
<code>{{ counter.count }}</code>
<div>
  <button (click)="counter.increment()">Increment</button>
  <button (click)="counter.reset()">Reset</button>
</div>

Il template è praticamente invariato, salvo il fatto che anziché recuperare proprietà e metodi dal componente, lo fa dall’istanza del servizio che il componente conserva nella proprietà counter.

Apparentemente la nostra applicazione è diventata più complessa, senza però cambiare di un millimetro. Mettiamo, però, caso che, in un altro punto dell’applicazione, sia necessario mostrare lo stato corrente del contatore; se avessimo voluto farlo con il codice precedente, avremmo dovuto usare una template variable per ottenere nel template app.component.html una reference al componente counter, e poi trasmettere il valore della sua property count giù per l’alberatura dei componenti fino al punto desiderato.

Per come stanno le cose ora, invece, è sufficiente richiedere un’istanza del servizio.

Vediamolo in un componente di esempio, other.component.ts:

import { Component } from '@angular/core';
import { CounterService } from './counter/counter.service';


@Component({
  selector: 'app-other',
  standalone: true,
  imports: [],
  template: '<p>{{ counter.count }}</p>',
})
export class OtherComponent {
  constructor(protected counter: CounterService) { }
}

Qui non c’è alcun riferimento al CounterComponent, né ci sono proprietà da cui il componente riceva il valore aggiornato: è sufficiente il servizio.

In app.component.html:

<app-counter />
<app-other />

È importante notare che in casi più concreti, il componente contatore che presenta e aggiorna lo stato dell’applicazione, non sarebbe verosimilmente vicino a tutti gli altri punti dell’app in cui lo stato dev’essere disponibile. Avere un’architettura che consente di disaccoppiare dati e funzioni rispetto alla loro collocazione nell’albero dei componenti offre un livello di flessibilità irraggiungibile senza servizi. In altri framework, risultati simili vengono ottenuti con diverse strategie, ad esempio i context in React.

La service class HTTP Client in Angular.

Una service class importante che Angular mette a disposizione è HttpClient; com’è facile immaginare, questo servizio ci consente di fare chiamate HTTP.

L’idea dietro questa classe è quella di racchiudere le API native per le comunicazioni HTTP (es. fetch) dietro a un servizio completamente integrato in Angular in modo da potervi aggiungere altre funzionalità attraverso il framework.

Usando HttpClient sarà possibile ad esempio simulare le risposte HTTP nei nostri test, oppure intercettare le richieste HTTP della nostra web app per modificarle; il caso di esempio più semplice è quello in cui tutte le richieste siano intercettate per aggiungere un token di autenticazione per l’utente.

In questa guida non affronteremo le pratiche di autenticazione, ma al di là di queste, l’uso di HttpClient nelle applicazioni Angular rappresenta la norma, anche perché semplifica parecchio le cose.

Un aspetto da considerare nell’uso di HttpClient, è che tutti i suoi metodi implementano risposte asincrone attraverso l’uso del tipo Observable, al posto del nativo Promise. Questi due tipi sono relativamente simili, ma gli Observable appartengono a RxJS, una sofisticata libreria di cui Angular fa uso per gestire tutto ciò che è basato su eventi, e dunque anche le risposte asincrone.

L’uso di Observable al posto di Promise consente ad Angular di introdurre alcune ottimizzazioni, ad esempio l’annullamento automatico delle richieste qualora il componente che le abbia innescate venga disattivato, ad esempio perché l’applicazione ha navigato su un’altra vista.

La documentazione ufficiale di Angular suggerisce di utilizzare HttpClient dietro a servizi dedicati e, quindi, non direttamente nei componenti; per gli esempi che andremo a fare, trasgrediremmo questa raccomandazione.

Per cominciare, configuriamo l’app in modo da fornire HttpClient via DI; in app.config.ts:

import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';


import { routes } from './app.routes';
import { provideClientHydration } from '@angular/platform-browser';
import { provideHttpClient } from '@angular/common/http';


export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideClientHydration(),
    provideHttpClient() // <- enable http client
  ]
};

Ora, iniettiamo HttpClient nel nostro app.component.ts, e facciamogli effettuare una richiesta GET:

import { Component, OnInit, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';


@Component({
  selector: 'app-root',
  standalone: true,
  imports: [],
  template: '<pre>{{data}}</pre>'
})
export class AppComponent implements OnInit {
  http = inject(HttpClient);
  data = '';


  ngOnInit(): void {
    this.http.get('https://catfact.ninja/fact')
      .subscribe(data => { this.data = JSON.stringify(data, null, 2) });
  }
}

Qui vediamo come il risultato di this.http.get() sia accessibile passando una callback al metodo subscribe, esattamente come avviene con Promise.then. Ci sono molte differenze tra Observable.subscribe e Promise.then, e alcune le vedremo più avanti, ma in questo specifico esempio i due metodi svolgono lo stesso ruolo.

Notiamo anche che HttpClient non è stato iniettato tramite il costruttore di AppComponent, ma usando la funzione inject; i due metodi sono sostanzialmente equivalenti, inject non è altro che una utility function messa a disposizione dal framework per risparmiarci di dichiarare un costruttore vuoto.

L’altra novità di questo esempio è il metodo ngOnInit che implementa l’interfaccia OnInit; questo metodo è un cosiddetto lifecycle hook, letteralmente un gancio al ciclo di vita del componente.
Gli hook più utilizzati sono OnInit e OnDestroy, ma ce ne sono altri, specifici per diverse casistiche. OnInit viene chiamato immediatamente dopo il costruttore del componente e a differenza di quest’ultimo, è particolarmente indicato per logiche più complesse o asincrone.

Ovviamente, poiché Observable non è un tipo nativo, non è possibile sfruttare async/await direttamente; una buona notizia è che gli Observable restituiti dai metodi di HttpClient possono essere facilmente convertiti in Promise; un’altra buona notizia è che nel 90% dei casi, questa conversione non sarà necessaria, grazie all’utility AsyncPipe. Vediamo immediatamente come fare queste due cose, salvo poi approfondirle in sezioni dedicate, sulle pipes e sugli Observables.

Ecco il codice dell’esempio riscritto in modo da usare async/await con la conversione a Promise:

import { Component, OnInit, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';


@Component({
  selector: 'app-root',
  standalone: true,
  imports: [],
  template: '<pre>{{data}}</pre>'
})
export class AppComponent implements OnInit {
  http = inject(HttpClient);
  data: string | null = null;


  async ngOnInit(): Promise<void> {
    const data = await firstValueFrom(this.http.get('https://catfact.ninja/fact'))
    this.data = JSON.stringify(data, null, 2);
  }
}

Qui abbiamo per l’appunto usato ngOnInit come metodo asincrono; il risultato di this.http.get() è stato convertito in Promise da firstValueFrom (più avanti vedremo perché questa funzione si chiama così).

Vediamo, ora, il secondo esempio:

import { Component, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { map } from 'rxjs';
import { AsyncPipe } from '@angular/common';


@Component({
  selector: 'app-root',
  standalone: true,
  imports: [AsyncPipe],
  template: '<pre>{{data | async}}</pre>'
})
export class AppComponent {
  http = inject(HttpClient);


  data = this.http.get('https://catfact.ninja/fact')
    .pipe(map(data => JSON.stringify(data, null, 2)))
}

Ora il nostro componente ha un aspetto abbastanza diverso:

  • OnInit è sparita, e data viene inizializzata direttamente usando this.http.get();
  • dopo il metodo http.get è stato appeso un altro metodo pipe(map(…));
  • nel template, non mostriamo più solo data, ma data | async, e abbiamo anche la AsyncPipe tra gli imports del componente

Vediamo di capirci qualcosa. Quello che abbiamo fatto essenzialmente, è cambiare il tipo della property data, da string a Observable<string>, cioè il tipo restituito da http.get(…).pipe(map(…)): qui, l’uso di pipe e map corrisponde ancora a Promise.then, non nel caso in cui venga usato per estrarre un valore, bensì per mutare il contenuto della promise restituendo un’altra promise.

In pratica, this.data è un’observable che nasce da una chiamata HTTP e poi viene trasformato da una pipeline di operatori, in questo caso composta da un solo map da Object a string.

Una cosa importante da capire sugli observables è che sono completamente passivi fino a che qualcuno chiama il loro metodo subscribe; dunque come fa la web app a mostrare i dati in questo caso? Chi chiama la subscribe? La risposta esatta è… la AsyncPipe!

Come vedremo a brevissimo, le pipes sono funzioni che possono essere utilizzate nei template per trasformare le espressioni usate nei binding. Qui la AsyncPipe sta facendo un duplice lavoro: da un lato attiva l’observable data, causando la chiamata HTTP e la trasformazione della risposta a stringa JSON; dall’altro, estrae il valore dell’observable e lo rende visibile nella vista.

In questo caso non possiamo apprezzarlo, ma la async pipe fa anche una terza cosa: annulla la subscribe quando il ciclo di vita del componente termina. Questa operazione non è necessaria per observable che si concludono dopo aver emesso un solo dato, ma un observable generalmente rappresenta un flusso di dati nel tempo che potrebbe anche non avere una fine, perciò dobbiamo cancellare la sottoscrizione quando non ha più senso ricevere aggiornamenti.

Hai bisogno di informazioni? 🙋🏻‍♂️

Parliamone! Scrivici su Whatsapp e risponderemo a tutte le tue domande per capire quale dei nostri corsi è il più adatto alle tue esigenze.

Oppure chiamaci al 800 128 626