back to top

Organizzare un’applicazione Angular attraverso i moduli

In questa punultima lezione mostreremo come creare dei nuovi moduli per meglio organizzare la nostra applicazione. Prima però ricapitoliamo cosa sono i moduli in Angular e quali sono le differenze rispetto ai moduli disponibili a partire da ES2015 e in TypeScript.

Cos’è un modulo in Angular

In Angular, un modulo è una classe a cui applichiamo il particolare decoratore @NgModule e permette di organizzare la nostra applicazione strutturandola in unità che contengono entità aventi una certa relazione fra loro. In questo modo possiamo raggruppare all’interno di un singolo contenitore componenti, direttive e pipe che sono in qualche modo collegati fra loro perché contribuiscono alla realizzazione di una certa funzionalità comune.

Ogni applicazione che creiamo con Angular CLI contiene già un modulo base (root module) che prende per convenzione il nome di AppModule e che abbiamo avuto modo di incontrare più volte nelle precendenti lezioni. Infatti ogni applicazione Angular deve avere almeno un modulo che può essere sufficiente nel caso di progetti di piccoli dimensioni, ma aggiungendo nuove funzionalità può essere utile ed opportuno creare dei nuovi moduli che consentono di ottenere una migliore organizzazione. Abbiamo già avuto modo di capire che Angular favorisce un approccio fortemente modulare e per accedere ad alcune funzionalità interne al framework è necessario importare dei nuovi moduli già definiti. Abbiamo avuto modo di sperimentare quanto appena affermato in varie occasioni, come nel caso dei form in cui abbiamo importato in AppModule il modulo FormsModule o ReactiveFormsModule a seconda dell’approccio scelto (Template-driven form oppure Reactive form).

Il Decoratore @NgModule

Ciò che contraddistingue un modulo è sostanzialmente il decoratore @NgModule che permette di definire le caratteristiche e i diversi componenti del modulo stesso. Al decoratore @NgModule passiamo come argomento un oggetto con le seguenti proprietà:

  • declarations contiene l’elenco dei componenti, direttive e pipe che appartengono al modulo.
  • imports è un array contenente i nomi di altri moduli le cui classi, che sono state esportate, sono impiegate nei template dei componenti dichiarati nel modulo corrente.
  • exports definisce quali degli elementi del modulo devono essere visibili all’esterno in modo da renderli disponibili ed utilizzabili all’interno dei template dei componenti appartenenti ad altri moduli. Un modulo può tranquillamente esportarne un altro senza nemmeno importarlo (un modulo può infatti avere altri moduli nell’array ‘exports’ che non devono necessariamente essere presenti nell’array ‘import’), ovviamente dovremo importare la classe richiesta attraverso la parola chiave ‘import’ messa a disposizione da TypeScript.
  • providers definisce l’elenco dei provider per la registrazione dei servizi definiti nel modulo corrente. La visibilità del servizio cambia a seconda della strategia di caricamento del modulo. (Approfondiremo questo argomento al termine della lezione)
  • bootstrap definisce qual è il componente root dell’applicazione. Questa proprietà è presente solo in AppModule.

Non tutte le proprietà appena descritte devono essere obbligatoriamente presenti.

Moduli Angular vs TypeScript (ES2015)

È bene sottolineare e tenere bene in mente che i moduli presenti in Angular (NgModules) sono diversi dai moduli disponibili in TypeScript e in generale in ES2015. Questi ultimi rappresentano una funzionalità del linguaggio di programmazione che permette di separare il codice in più file. Possiamo quindi accedere alle entità esportate da un modulo con ‘export’, importandole tramite la sintassi che fa uso della parola chiave import. Angular utilizza i moduli TypeScript (ES2015) e solitamente è presente un singolo componente, direttiva, pipe, servizio ecc… per modulo, ma introduce anche i propri moduli (NgModules) per meglio strutturare ed organizzare un’applicazione. A ciascun NgModule appartengono le classi inserite nell’array declarations e i servizi registrati tramite i provider dell’array providers. I componenti di un modulo potranno accedere alle funzionalità esposte dai moduli importati e allo stesso tempo un modulo può rendere visibile all’esterno una delle sue classi attraverso l’array exports.

Creare un nuovo modulo con Angular CLI (feature module)

Angular è un framework che promuove un’elevata modularità delle applicazioni. È infatti possibile creare dei moduli secondari oltre al root module AppModule per separare le diverse funzionalità presenti in un’applicazione. Questi moduli prendono il nome di Feature Module. Per esempio potremmo creare un modulo apposito per gestire il login di un utente.

Immaginiamo allora di aver iniziato a creare con il comando ng new un’applicazione che permette ad un utente di ordinare delle pizze online. Vogliamo realizzare un nuovo modulo che contiene i componenti e le pipe relative alla sezione che mostra a video il menù con l’elenco delle pizze disponibili. Vediamo come generare questo nuovo modulo sempre attraverso Angular CLI.

Supponendo di essere all’interno della cartella base del nostro progetto, possiamo iniziare a creare il nuovo modulo con il seguente comando:

ng generate module pizza
CREATE src/app/pizza/pizza.module.ts (189 bytes)

Verrà quindi generato automaticamente il file pizza.module.ts con il seguente contenuto

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

@NgModule({
  declarations: [],
  imports: [
    CommonModule
  ]
})
export class PizzaModule { }

Notiamo che al contrario di quanto avviene in AppModule, in questo caso importiamo CommonModule invece di BrowserModule. CommonModule esporta le direttive e le pipe più comuni che potranno servirci nei template dei componenti del modulo. Al contrario BrowserModule contiene anche le funzionalità necessarie solo nel Root Module per completare il rendering dell’applicazione nel browser. Ma abbiamo visto che anche in AppModule avevamo accesso a direttive come NgIf o NgFor. Ciò è reso possibile dal fatto che BrowserModule presenta nell’array exports dell’oggetto passato al suo decoratore @NgModule() proprio il modulo CommonModule, consentendo così ad AppModule, che lo importa, di accedere alle funzionalità esportate da CommonModule.

A questo punto possiamo generare una serie di componenti che appartengono al modulo appena creato.

ng generate component pizza/pizza-list
CREATE src/app/pizza/pizza-list/pizza-list.component.css (0 bytes)
CREATE src/app/pizza/pizza-list/pizza-list.component.html (29 bytes)
CREATE src/app/pizza/pizza-list/pizza-list.component.spec.ts (650 bytes)
CREATE src/app/pizza/pizza-list/pizza-list.component.ts (287 bytes)
UPDATE src/app/pizza/pizza.module.ts (279 bytes)
ng generate component pizza/pizza-list-item
CREATE src/app/pizza/pizza-list-item/pizza-list-item.component.css (0 bytes)
CREATE src/app/pizza/pizza-list-item/pizza-list-item.component.html (34 bytes)
CREATE src/app/pizza/pizza-list-item/pizza-list-item.component.spec.ts (679 bytes)
CREATE src/app/pizza/pizza-list-item/pizza-list-item.component.ts (306 bytes)
UPDATE src/app/pizza/pizza.module.ts (297 bytes)
ng generate component pizza/pizza-search   
CREATE src/app/pizza/pizza-search/pizza-search.component.css (0 bytes)
CREATE src/app/pizza/pizza-search/pizza-search.component.html (31 bytes)
CREATE src/app/pizza/pizza-search/pizza-search.component.spec.ts (664 bytes)
CREATE src/app/pizza/pizza-search/pizza-search.component.ts (295 bytes)
UPDATE src/app/pizza/pizza.module.ts (587 bytes)

Aggiungiamo anche una pipe personalizzata al nuovo modulo.

ng generate pipe pizza/pizza-discount         
CREATE src/app/pizza/pizza-discount.pipe.spec.ts (216 bytes)
CREATE src/app/pizza/pizza-discount.pipe.ts (215 bytes)
UPDATE src/app/pizza/pizza.module.ts (265 bytes)

Possiamo allora esaminare nuovamente il contenuto del file pizza.module.ts contenente la definizione del modulo appena creato.

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

import { PizzaDiscountPipe } from './pizza-discount.pipe';
import { PizzaListComponent } from './pizza-list/pizza-list.component';
import { PizzaListItemComponent } from './pizza-list-item/pizza-list-item.component';
import { PizzaSearchComponent } from './pizza-search/pizza-search.component';

@NgModule({
  declarations: [
    PizzaDiscountPipe,
    PizzaListComponent,
    PizzaListItemComponent,
    PizzaSearchComponent
  ],
  imports: [
    CommonModule
  ]
})
export class PizzaModule { }

Se supponiamo di voler utilizzare la direttiva NgModel in uno dei componenti, per esempio PizzaSearchComponent, dovremo allora importare il modulo FormsModule in PizzaModule. Inoltre per fare in modo che i componenti siano accessibili nel template degli altri moduli che importano PizzaModule, dovremo elencare i nomi delle classi nell’array exports come mostrato sotto.

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

import { PizzaDiscountPipe } from './pizza-discount.pipe';
import { PizzaListComponent } from './pizza-list/pizza-list.component';
import { PizzaListItemComponent } from './pizza-list-item/pizza-list-item.component';
import { PizzaSearchComponent } from './pizza-search/pizza-search.component';

@NgModule({
  declarations: [
    PizzaDiscountPipe,
    PizzaListComponent,
    PizzaListItemComponent,
    PizzaSearchComponent
  ],
  imports: [
    CommonModule
  ],
  exports: [
    PizzaListComponent,
    PizzaListItemComponent,
    PizzaSearchComponent
  ]
})
export class PizzaModule { }

Dopo quest’ultimo passaggio, siamo pronti ad importare il modulo appena creato all’interno del modulo base AppModule.

import { PizzaModule } from './pizza/pizza.module';
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    PizzaModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Feature Module vs Root module

All’interno di un’applicazione esiste un solo Root Module che per convenzione prende il nome AppModule. Solo al decoratore @NgModule() di quest’ultimo passeremo un oggetto avente una proprietà bootstrap che indica qual è il root component. Nel Root Module importiamo il modulo BrowserModule che permette di completare le operazioni di configurazione iniziale dell’applicazione per l’avvio nel browser. Al contrario in ciascun Feature Module, che creiamo per meglio strutturare l’applicazione, al posto di BrowserModule importiamo CommonModule che invece contiene solo informazioni per direttive comuni come NgIf e NgFor.

Moduli, Servizi e Dependency Injection

I moduli rappresentano un ottimo modo per meglio organizzare le nostre applicazioni, ma mentre i componenti, le direttive e le pipe in essi presenti hanno visibilità limitata al modulo stesso, a meno che non si chiede il contrario elencandoli esplicitamente nell’array exports del modulo, è bene fare un discorso a parte per i servizi.

Abbiamo visto in una delle precedenti lezioni che al fine di usare un servizio, dovremo registrarlo attraverso un Provider con uno degli Injector della gerarchia creata da Angular nell’applicazione. Può quindi sorgere spontanea la domanda: cosa succede se aggiungiamo un servizio all’array providers di un modulo diverso da AppModule o se assegniamo il nome di un modulo alla proprietà providedIn dell’oggetto di metadati del decoratore @Injectable di un servizio?

La risposta a questa domanda dipende dalla modalità di caricamento del modulo, ovvero se si tratta di un modulo che viene caricato immediatamente durante l’avvio dell’applicazione (eagerly loaded) oppure caricato su richiesta solo se necessario (lazy loaded).

Non abbiamo ancora visto come caricare un modulo a richiesta, ma nella prossima lezione vedremo un caso in cui avremo a che fare con moduli di questo tipo quando usiamo Angular Router. Per il momento vediamo solo dal punto di vista concettuale qual è il comportamento di Angular a seconda del tipo di modulo e del modo in cui registriamo un servizio.

In particolare nel caso in cui un servizio viene inserito nell’array providers di un modulo da noi creato (o se si usa la proprietà providedIn a cui assegniamo il nome del modulo stesso) e importato in AppModule, il servizio sarà registrato con il Root Injector.

Schema funzionamento moduli e servizi nel caso di un modulo caricato all'avvio dell'app

Se successivamente usiamo il servizio in un modulo caricato a richiesta (Lazy Module) che non importa il modulo a cui ‘appartiene’ il servizio, verrà usata l’unica istanza dell’applicazione anche nel Lazy Module.

Se invece aggiungiamo il servizio anche all’array dei providers del modulo Lazy, verrà creata una nuova istanza del servizio, registrata con il Child Injector, che ha visibilità limitata al modulo caricato a richiesta.

Schema funzionamento moduli e servizi nel caso di un modulo caricato a richiesta

Se infine un servizio viene aggiunto all’array dei providers di un modulo condiviso, saranno anche in questo caso create più istanze, una comune per tutti i moduli eagerly loaded e un’istanza registrata con il Child Injector di ogni Lazy Module che ha importato il modulo condiviso.

Schema funzionamento moduli e servizi nel caso di un modulo condiviso

Conclusioni

In questa lezione abbiamo visto come creare un nuovo modulo e che ruolo hanno le diverse proprietà dell’oggetto di metadati passato al decoratore @NgModule. Abbiamo inoltre spiegato quali sono le differenze fra Feature Module e Root module. Abbiamo infine illustrato brevemente che implicazioni hanno i moduli creati sulla visibilità dei servizi a seconda del metodo di caricamento dei moduli stessi. (Lazy Loading vs Eager Loading)

Pubblicitร