I moduli sono una delle feature che più amo di Angular: ci permettono di racchiudere componenti, servizi, pipe e direttive all’interno di un unico contenitore. Ma non si tratta soltanto di mettere dei file sotto una cartella comune! Un modulo può rappresentare una vera e propria sotto-parte di applicazione (tipicamente sfruttando il Lazy Loading), o può fornire un set di strumenti da utilizzare.

Abbiamo già visto in un altro articolo come creare moduli configurabili dall’esterno utilizzando gli InjectionToken e sfruttando una prassi comune: la creazione di un metodo forRoot(). Oggi voglio farvi vedere dei trucchetti per verificare se l’utilizzatore dei vostri moduli li sta importando correttamente.

Verificare che un modulo sia singleton

Esiste un trucchetto per verificare che un modulo sia singleton all’interno della vostra applicazione. Se non sapete cosa voglia dire singleton, significa che del modulo dev’essere presente una sola istanza.

Dato che in Angular non siamo noi a dover chiamare il costruttore dei moduli con new, non possiamo realmente adottare questo design pattern. L’utilizzatore può importare i nostri moduli ovunque, e noi non possiamo far nulla per proteggerci da un utilizzo errato. Ma possiamo verificarlo!

Attraverso un semplice check sul costruttore del nostro modulo, in questo modo:

import { NgModule, Optional, SkipSelf } from '@angular/core';

@NgModule(...)
export class CoreModule {

  constructor(@Optional() @SkipSelf() self: CoreModule) {
    if (self) {
      throw new Error('You cannot import CoreModule more than once.');
    }
  }
}

Confusi? Un attimo, vi spiego.

Stiamo provando a iniettare CoreModule all’interno di CoreModule. So già quali sono i vostri dubbi:

  • Si possono iniettare dei moduli?!” Sì, certo, e anche componenti, direttive e pipe.
  • Ma come fa Angular a iniettare una cosa quando la sta creando in quel momento?” Non può! Questo è il nostro obiettivo: se ci riesce, vuol dire che un CoreModule è già stato creato in un altro injector, e quindi lanciamo un errore per avvisare lo sviluppatore.
  • Ok, ma quei decoratori strani?” Ecco a cosa ci servono:
    • SkipSelf dice al motore di Dependency Injection di cercare quella dipendenza solo in altri injector, non in quello attuale. Senza questo decoratore, il nostro trucchetto non servirebbe a nulla, la nostra sarebbe una richiesta impossibile dato che CoreModule in quel momento deve essere ancora creato.
    • Optional dice ad Angular di non preoccuparsi se non trova la nostra dipendenza (al posto di andare in errore, restituisce un valore nullo): è proprio il caso ideale, vuol dire che CoreModule non è stato importato precedentemente.

Non l’ho chiamato CoreModule a caso: si tratta di una best practice ormai molto diffusa, potete trovare qualche informazione in più in quest’altro articolo.

Verificare che forRoot() venga chiamato una sola volta

Oltre alla creazione di un CoreModule, c’è un’altra pratica molto diffusa, che viene utilizzata anche dal team di Angular: quando vogliamo rendere un modulo configurabile, creiamo un metodo ad-hoc che ci restituisca un ModuleWithProviders, ovvero un modulo con l’aggiunta di servizi.

@NgModule(...)
export class LibraryModule {

  static forRoot(config): ModuleWithProviders<LibraryModule> {
    return {
      ngModule: LibraryModule,
      providers: [
        { provide: LIBRARY_CONFIG, useValue: config }
      ]
    };
  }
}

Non entro nel dettaglio perché ne abbiamo già parlato in un altro articolo: se avete dubbi, rileggetelo qui prima di continuare.

In questo caso non ha più senso parlare né di singleton né di fare dei controlli nel costruttore: il nostro modulo potrebbe essere importato correttamente più volte in vari moduli, magari per utilizzare dei suoi componenti o direttive!

Quello che ora non vogliamo è che l’utilizzatore chiami il metodo forRoot più di una volta. Come effettuare questo controllo? Ve lo dico subito, prendendo spunto dal metodo che usano il team di Angular e il team di NgRx.

Vi faccio vedere l’esempio completo, e poi lo discutiamo.

export const LIBRARY_FORROOT_GUARD = new InjectionToken<void>('LIBRARY_FORROOT_GUARD');

export function provideForRootGuard(library: LibraryService): any {
  if (library) {
    throw new Error(
        `LibraryModule.forRoot() called twice. Lazy loaded modules should use LibraryModule.forChild() instead.`);
  }
  return 'guarded';
}

@NgModule({
  ...
})
export class LibraryModule {

  constructor(@Optional() @Inject(LIBRARY_FORROOT_GUARD) guard: any) {}

  static forRoot(config): ModuleWithProviders<LibraryModule> {
    return {
      ngModule: LibraryModule,
      providers: [
        {
          provide: LIBRARY_FORROOT_GUARD,
          useFactory: provideForRootGuard,
          deps: [[LibraryService, new Optional(), new SkipSelf()]]
        },
        LibraryService,
        ...
      ]
    };
  }

  static forChild(config): ModuleWithProviders<LibraryModule> {
    return {
      ngModule: LibraryModule,
      providers: [ ... ]
    };
  }
}

Lo so, lo so, serve una spiegazione! Ecco cosa sta succedendo:

  1. Abbiamo creato un InjectionToken di cui facciamo il provide solo in forRoot: questo token rappresenta la nostra “guardia”, ovvero il nostro controllo. Il metodo forChild è totalmente opzionale ma l’ho incluso nel caso vi dovesse servire per passare un’ulteriore configurazione al modulo. Quest’ultimo metodo non fa il provide della guardia.
  2. Il nostro modulo deve avere un servizio singleton (altrimenti non avremmo bisogno di tutta questa pappardella!): nel mio caso, LibraryService. Questo servizio viene passato nei provider solo da forRoot.
  3. La nostra guardia ha uno scopo ben preciso: controllare se LibraryService esiste. Se esiste, forRoot è stato già chiamato, semplice! Per fare ciò non ci serve una classe, ci basta una funzione: nel mio caso, provideForRootGuard. Non ci interessa il valore di ritorno, ci basta lanciare l’errore se serve.
  4. Quest’ultima funzione ha bisogno di LibraryService, quindi dobbiamo indicarlo nelle dipendenze alla proprietà deps. Ma non ci basta passare LibraryService: vi ricordate che con CoreModule, all’inizio dell’articolo, abbiamo dovuto usare Optional e SkipSelf? Stessa storia, anche qui ci servono per gli stessi esatti motivi. La sintassi per specificare dei decoratori ad una dipendenza in deps è quella che vedete nel codice sopra, con un array in cui il primo valore è la dipendenza e i successivi sono i decoratori. Ma in questo caso, dobbiamo istanziarli noi, con new (un decoratore è una funzione!).
  5. Il passaggio finale: iniettiamo la guardia nel costruttore del modulo, altrimenti non verrà mai richiesta da nessuno (e la nostra funzione provideForRootGuard non sarà mai chiamata). In questo modo, ogni volta che l’utilizzatore importerà il nostro modulo, verrà eseguito questo controllo.

Difficile? Devo ammetterlo, un pochino… Ma spero siate riusciti a capire i vari passaggi! Se così non dovesse essere, vi basterà copiare il mio codice e fare queste modifiche:

  • LibraryModule dovrà essere rinominato con il nome del vostro modulo.
  • Rinominate anche LIBRARY_FORROOT_GUARD togliendo “LIBRARY” e usando il nome del vostro modulo.
  • Al posto di LibraryService, utilizzate un vostro servizio che dovrà essere globale a livello di applicazione (esempio: nel caso di RouterModule, il team ha usato il servizio Router. Nel caso di NgRx, è stato utilizzato il servizio Store).

Conclusioni

C’è poco da dire: so che questo ultimo capitolo potrebbe avervi scombussolato il cervello. Ma so anche quanto è importante sforzarsi e studiare codice altrui (nel mio caso il team Angular, non gli ultimi arrivati) per arricchirsi e scoprire nuove strategie. In questo caso, si tratta di codice che potete praticamente copia-incollare nel vostro progetto: siete stati fortunati! 😀

Ti è piaciuto l’articolo? Condividilo!

Rilasciare risorse gratuite non è facile: un sacco di tempo vola fra la ricerca dell’idea, la stesura, la preparazione di codice funzionante e la revisione. Se ti piace ciò che facciamo, condividi l’articolo: nel peggiore dei casi ti daranno del secchione!

Ti interesserà anche…

Angular

NgRx: cosa cambia nella versione 8

L’ottava versione di NgRx ha portato molti cambiamenti interessanti che ci consentono di scrivere codice più pulito e più snello. E c’è anche qualche chicca che vale la pena di…
Angular

CoreModule contro providedIn

Da tempo siamo abituati a creare nelle nostre applicazioni Angular un modulo specifico: CoreModule, ovvero il modulo che “sgrassa” AppModule consentendoci una maggiore pulizia. Ma ha ancora senso usarlo, ora…

Iscriviti alla nostra newsletter!

Rimani aggiornato per quando rilasceremo nuovi articoli, nuovi corsinuove promozioni e nuove risorse gratuite. Ti promettiamo che useremo la tua email con prudenza, e non la condivideremo con nessuno senza il tuo consenso!

Abbiamo a cuore la tua privacy. Niente spam. Leggi la nostra privacy policy qui.

Menu