Introduzione ad Angular 2 – Parte 2
Nel primo post introduttivo abbiamo affrontato i concetti base di Angular 2 come i Componenti, i Template e soprattutto il modo in cui comunicano tramite il nuovo two-way databindig. Se questi elementi si occupano di gestire l’interazione utente, i Servizi invece si preoccupano di reperire i dati da mostrare o inviare gli input al server.
Ci manca quindi da vedere come i servizi operano tramite HTTP per inviare o ricevere dati dal server; nell’ottica di una Single Page WebApp affronteremo anche il routing all’interno dell’applicazione, ma prima di tutto cerchiamo di capire come funziona il bootstrap di una applicazione Angular 2.
Angular2 RC1
Tra il primo post e questo, Angular2 è passato in RC1, dove sono state apportate alcune modifiche: le più evidenti sono che adesso i moduli Angular2 cambiano nome e da angular2/* diventano @angular/*. Altra modifica interessante riguarda il sistema di Routing che è stato aggiornato, ma la documentazione è ancora indietro: presenta qualche differenza rispetto alla versione beta, cercheremo quindi di parlare già della versione definitiva, in base a quel poco di documentazione che si trova in rete.
Bootstrap
Nel post precedente abbiamo imparato a conoscere l’applicazione TodoListApp come esempio per affrontare gli argomenti: il concetto che ne emerge è che ogni applicazione Angular 2 ha una struttura gerarchica in cui ogni componente sa come trattare i propri figli. Ma in cima a questa gerarchia chi ci sta? Avevamo visto che il componente principale era AppComponent
, agganciato ad uno specifico selector:
@Component({ selector: 'todo-list-app', ... }) export class AppComponent { ... }
Da qualche parte quindi esiterà un tag
e qualcun altro istanzierà AppComponent
in modo che sappia come trattare il tag. Visto che l’applicazione gira su un browser, sicuramente sarà una pagina html (proprio la index.html) a fare da “big bang”:
Angular 2 TodoListApp Loading...
Ed in particolare, sarà SystemJS (un noto module loader) a caricare il primo modulo, dal quale poi verranno richieste tutte le risorse necessarie in cascata. Inaspettatamente però il primo import non viene fatto su AppComponent
ma bensì sullo script di bootstrap main.ts nella cartella app (nel file systemjs.config.js, che vedremo più avanti, è specificata questa associazione):
import {bootstrap} from '@angular/platform-browser-dynamic'; import {AppComponent} from './app.component'; import {HTTP_PROVIDERS} from "@angular/http"; // Add all operators to Observable import 'rxjs/Rx'; bootstrap(AppComponent, [HTTP_PROVIDERS]);
il quale utilizza la funzione bootstrap
per l’inizializzazione da browser e istanzia AppComponent
insieme ad un vettore di componenti da cui dipende. Questo tipo di inizializzazione ricorda molto il bootstrap “manuale” di AngularJS tramite la funzione angular.bootstrap
.
Da notare le due righe evidenziate che approfondiremo a breve:
-
HTTP_PROVIDERS: si tratta di un insieme di provider per l’inizializzazione dei servizi Http, aggiunti come dipendenza alla nostra applicazione. Lo stesso effetto si sarebbe avuto nel dichiararlo direttamente sul
AppComponent
:@Component({ selector: 'todo-list-app', providers: [HTTP_PROVIDERS], ... }) export class AppComponent { ... }
- l’import di rxjs/Rx permette di sfruttare l’API completa di RxJS da cui i servizi Http dipendono.
Secondo la logica gerarchica dei componenti (e dei relativi injector), dichiarare questi import a livello di bootstrap permette di propagare queste funzionalità a tutta l’applicazione.
Riassumendo quindi, i passaggi logici che permettono di avviare una applicazione Angular 2 sono:
Il servizio Http
Già AngularJS ci aveva insegnato a suddividere le responsabilità applicative in controller e servizi, e Angular 2 non è da meno. Per comunicare con il server è sempre bene creare un nostro servizio applicativo nel quale iniettiamo l’Http Client di Angular: è buona prassi non usare Http in un controller!
Riprendendo l’esempio della TodoListApp del post precedente, possiamo reimplementare il servizio TodoListService
in modo che richieda al server la lista dei task da mostrare:
import {Injectable} from "@angular/core"; import {Todo} from "../models/todo"; import {Http} from "@angular/http"; import {Observable} from "rxjs/Observable"; @Injectable() export class TodoListService { constructor(private http: Http) {} getAll() { return this.http.get('/todos') .map(res => res.json()) .do(data => console.log(data)) // log results in the console .catch(res => { console.error(res.toString()); return Observable.throw(res.message || 'Server error') }); } }
Abbiamo già conosciuto la Dependency Injection, quindi la prima riga evidenziata non è un mistero. Ma chi fa da provider del servizio? Ricordate la dipendenza HTTP_PROVIDERS in fase di bootstrap? Si tratta di un vettore di providers fornito da Angular 2 per facilitare l’inizializzazione dei servizi legati alla comunicazione HTTP, come il servizio di tipo Http che possiamo iniettare nelle nostre classi. Ricordiamo che essendo stato dichiarato sul componente di root, l’istanza di Http è unica per tutta l’applicazione.
Da notare invece adesso la seconda riga evidenziata: siamo abituati da AngularJS ad aspettarci una Promise sulla risposta ad una chiamata al server. In questo caso invece abbiamo il metodo map
: per chi è abituato alla programmazione con le “Reactive Extensions” (come per esempio RxJava di cui abbiamo parlato ampiamente) avrà subito il sospetto che la chiamata http restituisca un Observable
! Ed è proprio così: Angular 2 ha come dipendenza RxJS, ma di default nella versione “slim“. Grazie all’import di 'rxjs/Rx' in fase di bootstrap visto in precedenza, possiamo usare tutte le potenzialità di RxJS. Perché questa scelta? RxJS non è un framework piccolo e includerlo tutto ha un certo costo di banda (soprattutto nel caso del mobile). In caso di tuning delle dimensioni dei file trasferiti, questo è il primo punto dove indagare per includere solo quella parte del framework che usiamo nella nostra applicazione.
Perché quindi getAll()
non restituisce direttamente l’Observablemap
servirà, non è una buona idea restituire la risposta http fuori dal servizio: il servizio infatti serve proprio per disaccoppiare anche a livello tecnologico chi forniscei i dati da chi li consuma. Nel caso della TodoListApp, TodoListComponent
è interessato alla lista dei task da mostrare, indipendentemente da che fonte dati arrivi: sta al servizio TodoListService
quindi preparare la risposta in tal senso. Nel caso di codice di risposta http al di fuori del range 200-300, il servizio http di Angular manda l’Observable in errore, per cui è sempre bene gestire il caso in modo da restituire un errore comprensibile a chi consuma il servizio:
this._todoListService.getAll().subscribe( (todos: Todo[]) => { todos.forEach(todo => this.todos.push(todo)); }, (error: string) => alert(error));
sottoscrivendo due callback: una per il caso corretto, una in caso di errore. Una cosa da tenere sempre a mente è che il servizio Http restituisce un cold Observable: significa cioè che la chiamata http non viene eseguita finché non c’è un subscriber che si registra ad esso! Per chi non se la sentisse di avventurarsi nel monto Rx, Angular permette di convertire facilmente un Observable
in una Promise
tramite il metodo Observable.toPromise()
.
Nel caso di salvataggio sul server, TodoListService
effettuerà una chiamata http POST:
import {Http, Headers} from "@angular/http"; ... store(todo: Todo) { let headers = new Headers({ 'Content-Type': 'application/json' }); return this.http.post('/todos', JSON.stringify(todo), { headers: headers }) .map(resp => resp.json()); }
Rispetto al servizio $http di AngularJS, Angular2 è un po’ più verboso perché l’obiettivo, a quanto pare, è quello di renderlo il più generico possibile: è necessario quindi serializzare esplicitamente l’oggetto da inviare in JSON e importare l’oggetto Headers
per specificarne il content type che non è più implicito come nella versione precedente. Effettivamente non è detto che tutte le API siano basate su JSON, però ormai sono la maggior parte…
Component Lifecycle Hooks
Nel paragrafo precedente abbiamo visto come richiamare dal server l’elenco dei task tramite il servizio Http, ma non abbiamo visto quando viene effettuata questa chiamata. Per essere sicuri di eseguire le operazioni appropriate nei momenti giusti, Angular2, durante la costruzione di un componente, definisce una serie di eventi a cui potersi registrare, come una sorta di “ganci“, da cui hooks.
A livello di implementazione, si traduce in una serie di interfacce con un solo metodo, una per ogni hook a cui si intende agganciarci. Il concetto di interfaccia ha solo senso in Typescript perché in JavaScript il transpiler non le riporta nemmeno! Di fatto quindi implementare o meno l’interfaccia a runtime non fa differenza perché questa sparisce: l’importante è che ci sia il metodo, perché è proprio quello che cerca Angular. Ovviamente è inutile dire che invece a livello di programmazione ha molto senso implementare l’interfaccia perché rende il codice molto più chiaro: sin dalla dichiarazione del componente si capisce subito a quali eventi si registra.
Angular cerca quindi i seguenti metodi di callback sui componenti e direttive secondo l’ordine:
- ngOnChanges: prima di ngOnInit e ogni volta che cambia una input property sul componente.
- ngOnInit: all’inizializzazione del componente.
- ngDoCheck: chiamato ogni volta che si verifica un ciclo di cambiamento.
- ngAfterContentInit: dopo la “proiezione” dei contenuti provenienti dall’esterno nella vista.
- ngAfterContentChecked: dopo il controllo dei contenuti provenienti dall’esterno su cui si è effettuato il binding.
- ngAfterViewInit: al termine della renderizzazione della vista (compresi i figli).
- ngAfterViewChecked: dopo il controllo dei binding sulle viste (compresi i figli).
-
ngOnDestroy: prima della distruzione del componente. Questo è il posto giusto dove fare gli unsubscribe degli
Observable
, per evitare memory leak.
I nomi delle interfacce da implementare corrispondono ai metodi appena visti senza il prefisso ng (per esempio OnInit, OnDestroy…).
Component Router
Con quanto abbiamo appreso finora siamo in grado di creare una pagina funzionante, ma ovviamente è troppo poco. Se stiamo realizzando una Single Page WebApp abbiamo bisogno di poter navigare tra le pagine (anzi, meglio definirle view, visto che la pagina è sempre la stessa) interne all’applicazione tramite un modulo che gestisce le rotte, che in Angular2 si chiama Component Router. Al momento la versione RC1 di Angular ha deprecato il Component Routing che introduce nel quickstart e la documentazione della nuova versione è scarsa. Fortunatamente in rete si trova qualcosa da cui possiamo prendere spunto.
Per abilitare le rotte abbiamo bisogno di:
- dichiarare il base path: serve per dire al browser come comporre gli url delle risorse. Deve essere il primo tag dell’header dell’html:
- una modalità per definire le rotte dell’applicazione, eventuali parametri e il componente associato alla rotta, tramite il nuovo decoratore @Routes (che sostituisce il deprecato @RouteConfig)
- una direttiva che identifica dove inserire il componente (view) che rappresenta la rotta, usata come tag:
A questo punto modifichiamo l’applicazione TodoListApp in modo da avere due pagine: una con la lista dei todo, una con la lista delle cose già fatte (done list). Per far questo, modifichiamo AppComponent
in modo da aggiungere la definizione delle rotte:
import {Component, OnInit} from "@angular/core"; import {Routes, Router, ROUTER_DIRECTIVES, ROUTER_PROVIDERS} from '@angular/router'; import {TodoListService} from "./services/todo-list.service"; import {TodosComponent} from "./todos.components"; import {DoneListComponent} from "./done-list.component"; @Component({ selector: 'todo-list-app', templateUrl: './app/app.component.html' directives: [ROUTER_DIRECTIVES], providers: [ ROUTER_PROVIDERS, TodoListService ] }) @Routes([{ path: '#/todos', component: TodosComponent },{ path: '#/done', component: DoneListComponent }]) export class AppComponent implements OnInit { public title = 'TodoApp'; constructor(private router: Router) {} ngOnInit() { this.router.navigate(['#/todos']); } }
Il decoratore @Routes accetta come argomento un vettore di RouteMetadata che definiscono il percorso della rotta (path) e il componente da attivare su di essa (component). Angular2 supporta l’hash routing e l’HTML5 routing: il primo è quello che prevede l’uso del carattere # (come in questo esempio), il secondo invece ne è privo. Sebbene quest’ultimo sia più semantico e leggibile, è anche quello più laborioso da gestire. L’accesso diretto ad una risorsa (come per esempio http://todolistapp.com/todos) non viene intercettato da Angular perché la chiamata va diretta al server sulla risorsa /todos: spetta al server quindi redirigere la chiamata alla pagina index.html per far attivare l’applicativo client e far sì che Angular la interpreti come una propria rotta. Nel caso di hash routing invece la chiamata è del tipo http://todolistapp.com/#/todos, ovvero il browser chiama la pagina index.html che gestirà internamente le rotte perché il simbolo # previene la chiamata al server.
Una volta dichiarate le rotte, possiamo definire quella iniziale iniettando il servizio Router e forzando programmaticamente la navigazione all’inizializzazione del componente (ngOnInit). Affinché Router sia iniettabile, sappiamo che deve essere dichiarato un providers per esso: come per Http, anche in questo caso Angular fornisce una costante, ROUTER_PROVIDERS, che racchiude una serie di provider di servizi per le rotte.
A questo punto non ci resta che modificare il template principale app.component.html in modo da aggiungere due voci di menù di navigazione e la direttiva
La direttiva routerLink crea automaticamente il link alla rotta specificata. Affinché venga correttamente interpretata dal rendering, è necessario specificare la costante ROUTER_DIRECTIVES tra le directives dell’AppComponent
.
Opzioni avanzate
Queste sono le nozioni base per implementare la navigazione all’interno della nostra applicazione, ma non sono sufficienti per l’uso di tutti i giorni. Component Router mette a disposizione altre funzionalità, come per esempio:
- nuovi hook di navigazione: l’interfaccia OnActivate definisce il metodo routerOnActivate chiamato al termine della navigazione in una nuova view (e prima di ngOnInit), mentre CanDeactivate definisce routerCanDeactivate chiamato prima di effettuare la navigazione (e distruggere il componente che si sta lanciando, quindi viene chiamato prima di ngOnDestroy). Quest’ultimo ritorna una Promise
per determinare se effettivamente si può effettuare la navigazione. - la possibilità di passare parametri:
@Routes([{ path: '#/todos/:id', component: TodosComponent }])
Open details
e recuperarli tramite l’oggetto RouteSegment (anche se sulla RC1 sembra che non sia iniettabile – genera errore -, ma si può recuperare come primo argomento di routerOnActivate.)
-
child routes: una delle novità della RC1 è che adesso è possibile definire rotte innestate all’interno di altre, come una sorta di sotto-risorse. Potrei quindi avere dei componenti che definiscono nuovamente @Routes come sotto-rotte della corrente e la view associata può definire un nuovo
dove inserire le sotto-viste.
Configurazione di un progetto
Fatti nostri i concetti principali, non ci rimane adesso che farci un’idea delle configurazioni necessarie per gestire il progetto. Nella directory root del progetto TodoListApp, sono presenti una serie di file di configurazione, secondo la documentazione ufficiale, che servono per impostare opportunamente il progetto, permettere la compilazione in JavaScript, avviare il watch sui file sorgente e il server di sviluppo. Tutto questo stack è necessario per avere un ambiente di lavoro veloce e reattivo alle modifiche. Cerchiamo di capire di cosa si tratta.
- package.json: (fin qui niente di nuovo) definisce le dipendenze del progetto e alcuni script di utilità.
- tsconfig.json: è il file di configurazione del compilatore TypeScript. Viene cercato automaticamente dal compilatore tsc nella cartella dove viene lanciato il comando o nei suoi parent (se non diversamente specificato). In questo file vengono definite le opzioni per il compilatore (compilerOptions) e quali percorsi escludere dalla compilazione (excludes), altrimenti verrebbero compilati tutti i file .ts, come per esempio la cartella node_modules e le definizioni in typings/main perché sono quelle server side.
-
typings.json: definisce il descrittore dei tipi di TypeScript, usato da Typings, il Definition Manager (installato in devDependencies). Dal momento che TypeScript è un super-set di JavaScript, è necessario che il compilatore converta il codice, e soprattutto sappia come farlo. Sono necessari quindi dei descrittori (*.d.ts) che fanno da mapping tra i tipi TypeScript e le funzioni JavaScript. Angular2 esce già con tutti i descrittori, ma non tutti i framework (come jQuery, Jasmine…) li hanno: è necessario quindi fornire questi descrittori in modo che il compilatore sappia trattarli. Grazie allo script “postinstall” definito nel package.json, dopo la fase di install delle dipendenze, Typings si preoccupa di installare nella cartella typings del progetto anche le definizioni inserite nel file di configurazione. Molte nuove definizioni possono essere trovate su GitHub o cercate direttamente tramite da riga di comando.
Lo script di partenza fa riferimento a es6-shim, jasmine e node all’interno delle ambientDependencies, che identifica le condizioni dell’ambiente in cui l’applicazione deve girare.
- systemjs.config.js: file di configurazione di SystemJS, uno dei module loader che si può scegliere per lavorare con Angular2.
Conclusioni
Con questi primi due post abbiamo visto solo la superficie di Angular2 perché mancano da affrontare altri argomenti interessanti come la creazione/gestione delle form o il testing, che cercheremo di affrontare prossimamente. A mio avviso Angular2 presenta molte semplificazioni dal punto di vista dei concetti, cosa sempre ben accettata, il che lo rende un framework più veloce da apprendere rispetto alla versione precedente. Trovo un po’ più complicata invece la gestione del progetto perché ha bisogno di diverse configurazioni prima di essere operativi: d’altra parte i progetti frontend sono molto più strutturati di qualche anno fa ed hanno bisogno di questo tipo di strumenti. La configurazione presentata con TodoListApp (che ricalca quella del Quickstart di Angular2) non è certo adatta alla produzione: vengono mischiati i file compilati insieme a quelli sorgente e le chiamate al server sono più di 300! Prossimamente, grazie ad Angular CLI, vedremo anche come ovviare a questi inconvenienti.
AGGIORNAMENTO luglio 2017: Angular 2 nell’arco di un anno è passato alla versione 4 e alcune cose descritte in questo post sono cambiate, soprattutto la parte che riguarda il Routing. Si consiglia pertanto di proseguire la lettura con
il post su Angular 4, che mette in evidenza le differenze della nuova versione con quanto descritto fin qua.