blexin

Sviluppo
Consulenza e
Formazione IT


Blog

Evolvi la tua azienda

Nuova versione del framework front-end targato Google: vediamo quali sono le principali novità introdotte

Angular 9: cosa cambia e perché usarlo

Mercoledì 8 Aprile 2020

Sappiamo che, nel nostro lavoro, il continuo aggiornamento dei framework e strumenti di sviluppo è fonte di gioie e dolori. In questo articolo, vedremo quali sono le novità di Angular 9, perché è una versione importante e quanto valga la pena provarla subito.

La pubblicazione della versione 9 era prevista per Novembre 2019, ma l’annuncio ufficiale di Stephen Fluin è arrivato solo il 6 Febbraio. Ciò ha suscitato molte critiche dovute al fatto che il team di Angular porta con sé una naturale diffidenza, erede di quella transizione drammatica da Angular.js alla versione 2.0.

Prima di entrare nel dettaglio, vi segnalo che il 26 Marzo è stata pubblicata anche la versione 9.1 che utilizzerò in questo articolo.

Ci rendiamo conto dell’importanza della versione 9 quando scopriamo che introduce per default Ivy, una nuova pipeline di compilazione e rendering (disponibile per esperimenti già dalla versione 8).

Perché questa riscrittura? Prima di tutto, il traffico web proviene in gran parte da smartphone e tablet. La sfida dei programmatori front-end è quella di caricare un’applicazione web il più velocemente possibile, considerando l’instabilità delle connessioni internet a nostra disposizione. Il primo passo è ridurre la dimensione del bundle prodotto dal framework, e Ivy raggiunge questo risultato generando meno codice per ciascun componente e utilizzando un algoritmo chiamato tree-shaking che rimuove le sezioni di Angular inutilizzate. I risultati sono importanti per applicazioni piccole e grandi mentre quelle di dimensione media non hanno ancora riduzioni sensibili.

Nuova esperienza di Debug

Creiamo un nuovo progetto utilizzando il comando ng new che si arricchisce di una opzione --minimal che rimuove tutti i file necessari per il testing.

ng new my-first-demo--minimal --style=css

Immaginiamo di avere un componente padre chiamato ProductListComponent e un componente figlio chiamato ProductDetails. A partire da questa situazione, vediamo come cambia l’esperienza di debug in Angular sfruttando i developer tool di Chrome.

Selezioniamo nel panel components il nostro componente padre e spostiamoci nella console.

Dalla console possiamo accedere a un nuovo oggetto chiamato ng, pensato per migliorare l’esperienza di debug. Partiamo associando a una variabile l’elemento selezionato disponibile mediante l’alias $0. Le api di ng restituiscono il componente mediante ng.getContext($0).

Possiamo clonare il primo prodotto, modificarlo e visualizzare le differenze con un console.table

e, infine, invocare ng.applyChanges($0) per eseguire la change detection in Angular. Volendo, possiamo invocare anche i metodi del componente selezionato, per simulare la pressione di un bottone.

Type checking

Typescript permette di identificare i type error nel codice ma finora mancava un supporto altrettanto forte nella parte di templating. Esiste, all’interno del file tsconfig.json, un flag chiamato fullTemplateTypeCheck che, nella versione 9 di Angular, è impostato a true. Il valore false di tale flag provoca il cosiddetto basic type-checking mode. Consideriamo il caso seguente:

<app-product-details
    [store]="user.address.city" [product]="selectedProduct">
</app-product-details>

Il compilatore controlla che:

  1. user sia una property della classe component
  2. user sia un oggetto con una proprietà address
  3. user.address sia un oggetto con una proprietà city

Nel basic type-checking mode, il compilatore non verifica che il valore di user.address.city sia assegnabile alla input store del componente figlio. Inoltre, non vengono controllate le cosiddette embedded view, ad esempio *ngIf, *ngFor e gli ng-template. Non vengono neppure controllati i risultati delle pipe e i tipi di $event negli event binding.

Quando il flag fullTemplateTypeCheck è uguale a true, Angular esegue un type checking più rigoroso. Le embedded view vengono controllate, le pipe hanno il corretto tipo di ritorno e tutte le reference locali alle direttive e pipe hanno il tipo corretto.

Prendiamo ad esempio il caso di un *ngFor:

<div *ngFor="let product of products">
    <h3>{{config.title}}</h2>
    <span>Store: {{user.address.city}}</span>
</div>

In basic mode, <h3> e <span> non vengono controllati. In modalità full, invece, il compilatore controlla che config e user esistano ma assume un tipo any per esse!

Angular 9 introduce una terza modalità di controllo, chiamata strict mode. Con essa il framework determina che user nello span è di un tipo User e che address è un oggetto con una proprietà city di tipo string.

Lo strict mode è accessibile solo con Ivy settando tsconfig.json nel modo seguente

"angularCompilerOptions":
{
    "strictTemplates": true,
    "strictInjectionParameters": true
}

Consideriamo il seguente esempio:

Ho intenzionalmente cambiato il case della proprietà id che nella definizione dell’interfaccia è invece id. Lasciando il tsconfig.json nella versione originaria

"angularCompilerOptions": 
{
    "fullTemplateTypeCheck": true,
    "strictInjectionParameters": true
}

la compilazione mediante comando ng build viene superata con successo.

Passando invece in strict mode, la compilazione fallisce.

La compilazione AOT

Abituati a lanciare le nostre app con il comando ng serve, quante volte ci siamo trovati di fronte ad errori di compilazione al momento di andare in produzione eseguendo il comando ng build --prod? Quello che succedeva era che con quest’ultima opzione veniva avviata la compilazione AOT (Ahead-Of-Time).

Ma perché abbiamo bisogno di una compilazione? Cosa viene compilato? Quando avviene? Il compilatore Angular può essere invocato a runtime (ossia nel browser dell’utente) o a build-time nel processo di build. Si è sempre parlato, infatti, della portabilità di Angular, ossia del fatto che il framework possa essere eseguito su qualsiasi piattaforma con una VM Javascript.

Nel primo caso (Just-In-Time Compilation) il flusso tipico è:

  • sviluppo del codice in Typescript
  • compilazione dell’applicazione con tsc
  • bundling, minification e deployment

L’utente apre l’app nel suo browser innescando i seguenti passi:

  • download di tutti gli asset Javascript
  • bootstrap di Angular
  • generazione del Javascript per ciascun componente dell’applicazione
  • rendering dell’applicazione

Nella compilazione Ahead-Of-Time i passi sono invece i seguenti:

  • sviluppo del codice in Typescript
  • compilazione dell’applicazione con ngc
  • bundling, minification e deployment

Quello che accade invece quando l’utente apre l’app nel suo browser è:

  • download di tutti gli asset Javascript
  • bootstrap di Angular
  • rendering dell’applicazione

La user experience è quindi migliore e più veloce perché un codice Javascript più efficiente viene generato in fase di build e il browser deve solo occuparsi del rendering. I dettagli di come funzioni la compilazione AOT vanno oltre gli scopi di questo articolo, ma vi confesso che trovo questo mondo davvero affascinante. Con Angular 9, la compilazione AOT con Ivy è così veloce ed efficiente che viene utilizzata anche con ng serve per le build in fase di sviluppo!

Dependency Injection (DI)

Altre novità importanti riguardano la Dependency Injection. La DI è cablata nel framework di Angular e usata ovunque per fornire a tutti i componenti i servizi di cui hanno bisogno. I componenti Angular consumano servizi iniettati attraverso il costruttore.

Qualsiasi classe può diventare un servizio. Se facciamo precedere la sua definizione da una decorazione @Injectable() possiamo modificare i metadati necessari ad Angular per iniettarla come dipendenza in un componente rispetto a quelli di default. @Injectable() serve ad indicare anche che una classe ha una dipendenza.

Angular nel suo processo di bootstrap crea un injector disponibile a tutta l’applicazione. L’injector crea le dipendenze e gestisce in un container tutte le istanze delle dipendenze da riutilizzare. Infine, un provider istruisce l’injector su come ottenere o creare una dipendenza.

Consideriamo come esempio il seguente costruttore di un componente:

constructor(private service: ProductsService) { }

Quando Angular scopre che un componente dipende da un servizio, controlla anzitutto che l’injector abbia una istanza disponibile di quel servizio. Se non c’è, ne viene creata una usando il provider registrato che viene aggiunta al container prima di passarla al componente.
Ogni servizio deve avere almeno un provider registrato a livello applicativo. Il provider può essere contenuto nei metadati del servizio (il decoratore @Injectable()) e in tal caso il servizio è disponibile ovunque nell’applicazione. In alternativa possiamo registrare dei provider a livello di modulo o componente (nei metadati di @NgModule o @Component).

Per default, la CLI di Angular con il comando ng generate service registra un provider col root injector, ossia viene creata una singola istanza condivisa del servizio e la inietta in ogni classe che ne faccia richiesta:

@Injectable({
  providedIn: 'root'
})

L’uso del root injector presenta anche il vantaggio di rimuovere tale istanza se non viene usata in nessun componente. All’interno di uno specifico modulo, possiamo registrare un provider che fornisca la stessa istanza a tutti i componenti del modulo stesso.

@NgModule({
  providers: [
  ProductsService
  ],
  ...
})

Quando registriamo un provider a livello di componente, creiamo una nuova istanza del servizio per ciascuna nuova istanza del componente.

@Component({
  selector: 'app-products-list',
  templateUrl: 'products-list.component.html',
  providers: [ProductsService]
})

La novità di Angular 9 è che quando creiamo un servizio @Injectable abbiamo due nuove opzioni per providedIn: platform ed any. Secondo la documentazione, l’opzione platform rende un servizio disponibile come singleton con un injector che è condiviso da tutte le applicazioni su una pagina. Non dimenticate, infatti, che la vostra pagina index.html della SPA può in principio ospitare più di un’applicazione Angular! Riusciamo in questo modo a condividere un servizio superando i famosi e invalicabili application boundaries!

L’opzione providedIn: any funziona in maniera tale per cui il servizio verrà fornito in ogni modulo in cui viene usato. Questo implica che ogni modulo caricato col lazy-loading (altra importante funzionalità offerta a partire da Angular 8) ha una istanza propria del servizio. Invece i moduli caricati normalmente (modalità eager) continueranno a condividere la singola istanza fornita dall’ injector del modulo root. Questa nuova opportunità per i moduli lazy-loaded è stata pensare per evitare che i moduli non interferiscano tra loro generando side-effect.

Lazy-Loading Components

Veniamo, infine, all’ultima novità che intendo mostrarvi. Siamo abituati a pensare ai moduli come cittadini di prima classe, anzi come il blocco principale di una qualsiasi applicazione Angular. In essi dichiariamo i componenti, le direttive, le pipe e i servizi. Un’applicazione non può esistere senza almeno un modulo. Ivy parte da un approccio diverso. Un componente può esistere senza un modulo. Tale concetto è chiamato locality: tutti i metadati necessari sono locali al componente.

Usiamo la ng CLI per generare un componente chiamato FirstLazy utilizzando le seguenti opzioni:

ng g c first-lazy --flat --skip-import --skip-selector

Abbiamo un unico file in cui è dichiarata la class FirstLazyComponent che non ha neppure un selector.

import { Component} from '@angular/core';
@Component({
    template:
      <p>
        first-lazy works!
      </p>
    `
})
export class FirstLazyComponent {
    constructor() { }
}

Il componente così creato non è registrato neppure nel root module (app.module.ts).

Come possiamo allora caricarlo? Anzitutto nell’ AppComponent inseriamo un bottone al cui click associamo una function definita nel codice.

@Component({
    selector: 'app-root',
    template: `
      <div>
          <div>My first lazy component </div>
          <button class="btn btn-info" (click)="showLazy()">Create</button>
      </div>
    `
})
export class AppComponent {
    title = 'my-first-demo';
    showLazy(){
    }
}

Nel costruttore possiamo iniettare una istanza di ViewContainerRef. Si tratta di un contenitore in cui possiamo aggiungere una o più view ad un componente. Iniettiamo anche un altro servizio chiamato ComponentFactoryResolver che permette di creare un componente da codice.

export class AppComponent {
    title = 'my-first-demo';
    constructor(private cfr: ComponentFactoryResolver,
                private viewContainerRef: ViewContainerRef) {
    }
    showLazy(){
    }
}

Nel metodo showLazy(), anzitutto svuotiamo il contenitore dalla presenza di eventuali componenti. Importiamo in maniera asincrona il codice del componente e lo creiamo utilizzando il ComponentFactoryResolver.

async showLazy(){
    this.viewContainerRef.clear();
    const { FirstLazyComponent } = await import('./first-lazy.component');
    this.viewContainerRef.createComponent(
      this.cfr.resolveComponentFactory(FirstLazyComponent)
    );
}

Abbiamo un’alternativa che ci da maggiore controllo sul container. Usiamo un ng-container a cui associamo una template reference variable.

template: `
    <div>
        <div>My first lazy component </div>
        <button class="btn btn-info" (click)="showLazy()">Create</button>
        <div class="myBackground">
          <ng-container #myContainer></ng-container>
        </div>
    </div>

Nel codice della classe usiamo l’annotazione @ViewChild e la impostiamo in maniera tale che il ViewContainerRef punti al nostro #myContainer. Modifichiamo quindi il codice del click del bottone.

export class AppComponent {
    title = 'my-first-demo';
 
    @ViewChild('myContainer', {read: ViewContainerRef}) myContainer: ViewContainerRef;
  
    constructor( private cfr: ComponentFactoryResolver) {}
 
    async showLazy(){
        this.myContainer.clear();
        const { FirstLazyComponent } = await import('./first-lazy.component');
        this.myContainer.createComponent(
          this.cfr.resolveComponentFactory(FirstLazyComponent)
        );
    }
}

Nel codice del componente lazy possiamo inserire una proprietà (l’ho chiamata message), e crearne un’istanza passando come parametro anche un Injector iniettato nell’ AppComponent. Su quest’istanza possiamo settare la proprietà message utilizzando un nostro servizio (ProductsService).

export class AppComponent {
    title = 'my-first-demo';
 
    @ViewChild('myContainer', {read: ViewContainerRef}) myContainer: ViewContainerRef;
  
    constructor(private cfr: ComponentFactoryResolver,
            private injector: Injector,
            private myservice: ProductsService) {
    }
 
async showLazy(){
    this.myContainer.clear();
    const { FirstLazyComponent } = await import('./first-lazy.component');
    const {instance} = this.myContainer.createComponent(
        this.cfr.resolveComponentFactory(FirstLazyComponent), null, this.injector);
    instance.message = this.myservice.message();
    }
}

Verifichiamo nelle attività di rete l’effettivo caricamento del file del lazy component.

Un lazy component può a sua volta caricare dei figli che siano essi stessi lazy component. Lo scenario è quello di poter gestire feature nella nostra applicazione senza ricorrere al routing. Perché farlo? Direi per avere un controllo maggiore nel caricamento in applicazione che consumano molte risorse.

Inevitabilmente, il nostro componente lazy prima o poi avrà bisogno di moduli esterni (pensiamo ad esempio a Material), Ma dove aggiungiamo questi moduli? Non possiamo caricarli in maniera eager nel nostro AppModule, pena un errore di build.

error TS-998001: 'mat-card' is not a known element:
1. If 'mat-card' is an Angular component, then verify that it is part of this module.
2. If 'mat-card' is a Web Component then add 'CUSTOM_ELEMENTS_SCHEMA' to the '@NgModule.schemas' of this component to suppress this message.

La soluzione è quella di aggiungere al file del componente un piccolo modulo senza dichiarazioni di esportazione. Il solo componente che questo modulo dichiarerà è il FirstLazyComponent.

@NgModule({
    declarations: [FirstLazyComponent],
    imports: [CommonModule, MatCardModule]
})
class MyLazyModule {
}

Questo trucco, giustificato dal fatto che abbiamo bisogno di un modulo per importare altri moduli, non sarà più necessario nelle prossime versioni di Angular. Il futuro di Angular sarà incentrato su Ivy e sul concetto di località, ossia permettere che un componente sia completamente autosufficiente. 

Spero che le novità di Angular vi siano piaciute e che possiate utilizzarle nelle vostre future applicazioni. Il codice dell’articolo è disponibile al seguente indirizzo.

Alla prossima!

Autore

Servizi

Evolvi la tua azienda