AngularJS è sempre più popolare tra chi si lavora allo sviluppo per il Web. Ne troviamo conferma anche analizzando le statistiche di ricerca di Google Trends. Il seguente grafico, ad esempio, mostra il confronto tra il numero di ricerche su Google che coinvolgono alcuni dei più popolari framework di sviluppo JavaScript:
Tuttavia l'interesse crescente verso questo framework non vuol dire che esso sia effettivamente utilizzato su larga scala. Il numero di applicazioni Web sviluppate con AngularJS non è attualmente proporzionale all'interesse suscitato. I motivi sono da ricercare da un lato alla relativa giovinezza del progetto dall'altro alla non facile digeribilità del modello di programmazione proposto quando si va oltre il classico "Hello world".
La curva di apprendimento
Di solito il primo approccio con AngularJS seguendo un tutorial o un workshop suscita un certo entusiasmo. Vedere come con minime istruzioni è possibile avere già un prototipo funzionante è uno stimolo notevole per suscitare l'interesse dello sviluppatore.
Spesso però ci si ferma alla superficie e si immagina che tutto sia fattibile con i quattro concetti appresi nel corso della presentazione, scontrandosi invece con la reale complessità del framework. Infatti, una cosa che normalmente non viene esplicitamente detta nelle presentazioni di AngularJS è che si tratta di un framework complesso, per la cui effettiva padronanza possono servire mesi di utilizzo sul campo.
In mancanza di una consapevolezza della complessità di questo framework, la curva di apprendimento non è lineare, come ben evidenzia il grafico semiserio pubblicato da Ben Nadel sul suo blog:
Differenza tra framework e librerie
Innanzitutto occorre fare una distinzione tra framework e librerie. Sembra una cosa banale, ma spesso chi lavora con JavaScript è abituato ad utilizzare librerie che semplificano la gestione di uno o più aspetti dello sviluppo.
Ad esempio, come è noto, jQuery è una libreria che semplifica la manipolazione del DOM, Underscore invece mette a disposizione funzioni di utilità nella manipolazione di array, oggetti ed altre problematiche comuni, D3.js è una libreria specializzata nel rendering grafico di dati.
Le aspettative dello sviluppatore sono legittime, ma non tengono conto del fatto che va seguita una certa logica, un certo modo di ragionare, quello che spesso viene indicato come the Angular way.
- Una libreria, quindi, è un insieme di funzionalità che semplificano lo sviluppo di una particolare problematica di programmazione;
- Un framework, al contrario, è più concentrato nel fornire una infrastruttura per lo sviluppo di applicazioni che offrire funzionalità per risolvere un problema specifico.
Possiamo dire che il problema principale che un framework intende risolvere è proprio l'organizzazione dell'architettura di un'applicazione.
Ecco, AngularJS è un framework di sviluppo JavaScript con particolare propensione al supporto di Single Page Application.
È importante fare questa distinzione tra framework e librerie perché molto spesso AngularJS viene confrontato con librerie come jQuery o altre simili con risultati fuorvianti. Confrontare AngularJS con una classica libreria è sintomo che si sta partendo con il piede sbagliato, perchè ci si attende qualcosa che il framework non prevede di offrire.
Pensare in termini architetturali
Uno degli errori più comuni è pensare di utilizzare Angular per creare delle pagine HTML a cui aggiungere un po' dinamismo con JavaScript. Questo approccio difficilmente ha successo con applicazioni Angular di una certa complessità. È opportuno pensare che le applicazioni Angular (e, più in generale, qualsiasi Single Page Application) sono "applicazioni client-side" e non "pagine Web".
E quando si crea un'applicazione non banale la sua architettura è fondamentale per la manutenibilità.
AngularJS mette a disposizione diversi elementi per definire l'architettura di un'applicazione: controller, servizi, direttive, filtri, ecc. Ma tutti si fondano su un concetto basilare dell'organizzazione del codice: il concetto di modulo.
Una delle best practice
della programmazione JavaScript consiste nell'evitare l'uso di variabili globali. L'utilizzo dei moduli ci consente allo stesso tempo di seguire questa regola fondamentale e di organizzare il nostro codice in unità eventualmente riutilizzabili.
Il modo più semplice per creare un modulo in AngularJS è il seguente:
angular.module("mioModulo", []);
Questa istruzione crea un modulo vuoto e senza dipendenze. Possiamo aggiungere al modulo i vari componenti che creano la nostra applicazione, come nel seguente esempio:
angular.module("mioModulo")
.controller("mioController", function() {
// ...
})
.service("mioServizio", function() {
// ...
});
È importante sottolineare che non è necessario che la creazione del modulo e l'aggiunta di elementi in esso avvenga nello stesso file fisico. Possiamo definire un modulo ed il suo contenuto in file diversi, disaccoppiando di fatto la corrispondenza tra file ed elementi architetturali dell'applicazione.
Questo aspetto offre una grande libertà nell'organizzazione del codice, ma purtroppo spesso può essere fonte di disorientamento.
Analizzare bene l'organizzazione del codice della propria applicazione è uno degli elementi di successo principali nello sviluppo di un'applicazione Angular.
MVC o MVW... a ciascuno il suo compito
Spesso si parla di AngularJS come di un framework MVC o, come lo ha definito uno dei suoi autori, MVW (Model-View-Whatever), per sottolineare l'estrema flessibilità nell'applicazione di un pattern di presentazione.
In realtà il supporto del noto design pattern è solo una delle caratteristiche di Angular. In ogni caso, il pattern definisce la separazione dei compiti nella presentazione dei dati all'utente individuando tre componenti fondamentali: il modello dei dati, la view ed il controller o qualunque altro elemento che fa da collante tra la view e i dati.
L'approccio della separazione dei compiti ha diversi vantaggi, tra cui la possibilità di modificare internamente un componente senza avere ripercussioni sull'altro.
Angular fa suo il principio di separazione delle competenze estendendolo ai vari componenti che contribuiscono alla costruzione di un'applicazione. In particolare, possiamo evidenziare i seguenti elementi principali:
Elemento | Descrizione |
---|---|
View | Rappresenta quello che l'utente vede, l'HTML risultante dalle elaborazioni di AngularJS |
Controller | È la logica che sta dietro alla View , il componente che mette in relazione i dati e la loro visualizzazione |
Service | È un componente che offre funzionalità agli altri componenti, indipendentemente da una View |
Directive | È un componente riutilizzabile all'interno di una View che estende l'HTML aggiungendo attributi o elementi personalizzati |
Filter | Formatta il valore di un'espressione per la visualizzazione su una View |
Oltre a questi esistono altri componenti che hanno prevalentemente un utilizzo interno o comunque hanno scopi molto particolari. In ogni caso, ciascun componente ha una finalità specifica ed occorre utilizzarlo per lo scopo per cui è stato pensato.
Ad esempio, uno degli errori più frequenti consiste nel prevedere istruzioni JavaScript che manipolano il DOM all'interno di un controller. Questo spesso genera comportamenti diversi da quelli attesi, come ad esempio la mancata modifica del DOM, se non addirittura il verificarsi di eccezioni.
È bene aver chiaro che un controller è semplicemente un componente di supporto alla view nella presentazione dei dati. Il suo ruolo fondamentale consiste nel fornire i dati alla view tramite lo scope
ed eventuali funzionalità di manipolazione dei dati.
Qualunque altra funzionalità diversa da questa deve essere delegata al componente più appropriato. Ad esempio, se abbiamo bisogno di intervenire sul DOM per creare un effetto sulla view, allora dobbiamo valutare la creazione di una nostra direttiva. Se invece abbiamo bisogno di creare una funzionalità generale riutilizzabile da più componenti della nostra applicazione, allora abbiamo bisogno di creare un servizio.
Anche per esigenze che potrebbero sembrare banali, come ad esempio la definizione di una costante globalmente accessibile a tutti i componenti di un'applicazione, richiede l'individuazione del componente adeguato. Nel caso specifico occorre creare un particolare tipo di servizio, constant
, come mostrato nel seguente esempio:
angular.module("mioModulo")
.constant("miaCostante", 123);
Insomma, capire bene il ruolo di ciascun componente fa parte della costruzione del pensiero Angular, aiuta a creare applicazioni con il comportamento atteso ed evita errori talvolta difficili da diagnosticare.
Link Utili
A partire dalla pagina seguente ci concentriamo sulla Dependency Injection e su altri elementi fondamentali del framework.
Dependency injection
La separazione delle competenze tra i componenti di un'applicazione può apparire un po' troppo rigida a prima vista. Dover creare un componente specifico per una funzionalità molto semplice, come ad esempio può essere una costante condivisa, può essere vista come una inutile complicazione, e probabilmente per certi versi lo è. Ma questo è il prezzo da pagare per ottenere il vantaggio della modularizzazione, cioè della creazione di componenti focalizzati su un compito specifico ed eventualmente riutilizzabili in applicazioni diverse.
La composizione di un'applicazione utilizzando moduli diversi è ottenuta tramite il pattern della dependency injection: un'altra delle feature più difficili da digerire per chi si avvicina ad AngularJS senza un'esperienza di programmazione basata sui design pattern.
Molto probabilmente la difficoltà nella comprensione della dependency injection deriva direttamente dalla non chiara conoscenza del concetto di modulo.
Infatti la dependency injection consente di combinare insieme componenti allo scopo di strutturare un'applicazione. Se all'interno di un componente Angular abbiamo bisogno delle funzionalità offerte da un altro componente non dobbiamo fare altro che dichiararne la dipendenza.
Ad esempio, se in un controller abbiamo bisogno di accedere alle funzionalità offerte da un servizio possiamo dichiararlo nel seguente modo:
angular.module("mioModulo")
.controller("mioController", function(mioServizio) {
// ...
});
È questo il classico esempio con cui dichiariamo la dipendenza di un controller dall'oggetto di sistema $scope
o dal servizio $http
per effettuare chiamate Ajax:
angular.module("mioModulo")
.controller("mioController", function($scope, $http) {
//...
});
Quasi sempre, in realtà, al posto del codice dell'esempio precedente vediamo il seguente codice semanticamente equivalente:
angular.module("mioModulo")
.controller("mioController", ["$scope", "$http", function($scope, $http) {
// ...
}]);
La necessità di passare come secondo argomento del costruttore di un controller (o di un'altro componente) un array di stringhe, il cui ultimo elemento è la funzione che ne implementa la logica, è dettata da esigenze pratiche legate alla minificazione del codice. Se infatti non si adottasse questo accorgimento, durante il processo che riduce la lunghezza del nome delle variabili si verrebbero a perdere i riferimenti ai nomi dei componenti che rappresentano le dipendenze.
Questo aspetto raramente viene spiegato ai neofiti di Angular che spesso si trovano ad utilizzare una sintassi così prolissa senza comprenderne il perché.
Infine, oltre a favorire la strutturazione a componenti, la separazione delle competenze e la dependency injection
semplificano la testabilità del codice isolando le varie funzionalità e predisponendo un'infrastruttura per l'utilizzo di mock negli unit test.
Approccio dichiarativo
La maggior parte degli sviluppatori JavaScript sono abituati a gestire l'interfaccia utente manipolando direttamente il DOM, eventualmente utilizzando librerie come jQuery, MooTools o altre. Viene naturale, quindi, pensare a come modificare un valore visualizzato su una casella di testo in termini di accesso alle proprietà dell'elemento del DOM.
AngularJS propone un approccio totalmente diverso nella manipolazione dell'interfaccia utente, un approccio dichiarativo basato sull'interpolazione di espressioni, sull'utilizzo di direttive e sul two-way data binding. Il classico esempio è quello basato su una view che contiene il seguente codice:
<div>
La somma di <input type="number" ng-model="addendo1" />
e <input type="number" ng-model="addendo2" />
è {{addendo1 + addendo2}}
</div>
Questo esempio consente all'utente di inserire due valori numerici nelle rispettive caselle di testo ed ottenere la relativa somma. Esso è perfettamente funzionante senza scrivere una riga di JavaScript.
L'approccio dichiarativo nella definizione di una view è molto potente e consente di ridurre la quantità di codice necessaria ad implementare le funzionalità richieste in una interfaccia utente. Bisogna tuttavia comprenderlo bene e saperlo gestire opportunamente.
Purtroppo molto spesso la disponibilità di questo approccio che sfrutta espressioni, direttive e filtri direttamente nell'HTML viene dimenticata o sottovalutata dagli sviluppatori. In questi casi, il codice aggiunto al controller della view tende ad aumentare divenendo di difficile comprensione e gestione.
Per quanto possibile occorrerebbe definire una view ragionando su cosa e quando deve essere visualizzato sfruttando le direttive ng-show
, ng-class
, ecc. evitando di cadere nella tentazione di manipolare il DOM dall'interno del controller.
Ad esempio, quando una view ha elementi che vanno visualizzati o nascosti in base a determinate condizioni, occorre individuare queste condizioni ed rappresentarle tramite espressioni JavaScript o variabili dello scope, come nel seguente esempio:
<form>
<input type="text" ng-model="cliente.nome"/>
<!-- ... -->
<input type="button" value="Salva" ng-click="salva()">
<input type="button" value="Elimina" ng-click="elimina()" ng-show="cliente.id > 0">
</form>
Viene visualizzata una form di gestione dei dati di un cliente e vengono presentati i pulsanti per salvare e per eliminare il cliente. La visualizzazione del pulsante di eliminazione, però, viene condizionato alla reale esistenza dell'oggetto cliente. Cioè, se siamo nella fase di creazione di un nuovo cliente, e quindi il valore del suo id
è 0, il pulsante non viene visualizzato.
Quindi, per quanto possibile, sfruttiamo l'approccio dichiarativo offerto da Angular nella definizione di una view limitando il ruolo del relativo controller essenzialmente al contenimento del modello e di funzioni di supporto ai dati. Teniamo presente che se il codice di un controller inizia a diventare complesso allora c'è qualcosa che non va.
La maggior parte dei programmatori JavaScript sono abituati a pensare con un approccio event-driven
: ogni prevedibile evento, generato dall'utente o dal sistema, deve avere il suo gestore.
AngularJS prevede alcune direttive per gestire opportunamente gli eventi di interazione dell'utente con l'interfaccia grafica: ng-click
, ng-change
, ng-mouseover
, ecc. Nella gestione degli eventi vanno assolutamente utilizzate queste direttive invece di tentare di gestirli secondo l'approccio standard o peggio tramite l'approccio jQuery.
In ogni caso, prima di imbarcarsi nella gestione di un evento, è opportuno chiedersi se è effettivamente necessario. Se una view è stata ben progettata, spesso non occorre ricorrere alla gestione di eventi. Ad esempio, l'impulso di utilizzare ng-change
per riportare sui dati la modifica fatta sull'interfaccia utente può essere forte, ma può risultare un intervento superfluo dal momento che il two-way binding si occupa di mantenere automaticamente la corrispondenza dei valori tra la view ed il modello dei dati.
La situazione può peggiorare quando si scopre la possibilità di monitorare una variabile di scope tramite $watch()
o di generare eventi personalizzati tramite $broadcast()
e $emit()
e di gestirli tramite $on()
. La tentazione di sfruttarli per riportare il modello di programmazione di Angular ad uno conosciuto e confortevole come quello event-driven è forte, ma l'uso non meditato di questi strumenti porta spesso a costruire applicazioni poco efficienti e talvolta con comportamenti non prevedibili.
Questo non vuol dire che la gestione degli eventi in Angular sia sempre da evitare. È opportuno valutare per bene la necessità di ricorrere agli eventi o ad altre funzionalità che espongono meccanismi interni di Angular, analizzando prima se l'infrastruttura di base del framework non permetta già di gestire la problematica.
Ad esempio, spesso la tentazione di ricorrere alla generazione e gestione degli eventi si ha quando si devono far interagire controller diversi. Supponiamo ad esempio di avere il seguente codice:
<div ng-controller="controller1">
<input ng-model="variabile1"/>
</div>
<div ng-controller="controller2">
<input ng-model="variabile2"/>
</div>
Vogliamo che il valore inserito dall'utente sulla prima casella di testo venga visualizzata anche sulla seconda casella e viceversa. Dal momento che i due controller hanno scope indipendenti, si potrebbe pensare di generare un evento tramite $broadcast()
nel primo controller e gestirlo tramite $on()
sul secondo controller e viceversa. Questa soluzione è certamente funzionante, ma coinvolge l'infrastruttura interna di Angular, dal momento che si deve accedere al $rootScope
, e può risultare inefficiente.
La soluzione migliore consiste nel creare un servizio che faccia da intermediario tra i due controller, come mostrato dal seguente codice:
angular.module("mioModulo")
.factory("intermediario", function() {
return {
obj: {
valore: ""
}
};
});
A questo punto i due controller potranno fare entrambi riferimento alla proprietà valore
messa a disposizione dal servizio intermediario
e sfruttare implicitamente il meccanismo di binding di Angular:
angular.module("mioModulo")
.controller("controller1", ["$scope", "intermediario", function($scope, intermediario) {
$scope.variabile1 = intermediario.obj;
}])
.controller("controller2", ["$scope", "intermediario", function($scope, intermediario) {
$scope.variabile2 = intermediario.obj;
}]);
Il codice HTML diventa quindi nel seguente modo:
<div ng-controller="controller1">
<input ng-model="variabile1.valore"/>
</div>
<div ng-controller="controller2">
<input ng-model="variabile2.valore"/>
</div>
Di fatto i modelli di entrambe le caselle di testo puntano allo stesso oggetto, ottenendo l'aggiornamento automatico.
Naturalmente il punto non è tanto l'esempio specifico, ma l'approccio in sè. La soluzione basata sugli eventi potrebbe essere valida per l'esempio specifico, data la sua semplicità. Ma applicato in situazioni più complesse può presentare problemi, soprattutto quando gli eventi generati e gli elemento coinvolti sono in numero considerevole.
L'atteggiamento giusto con AngularJS
Lungi dall'essere esaustiva, questa carrellata sullla visione Angular dello sviluppo di Single Page Application vuole mettere in guardia da un approccio troppo disinvolto nell'uso del framework, senza comprenderne per bene i fondamenti.
Utilizzare AngularJS continuando a pensare di avere sotto mano la libreria JavaScript impiegata nell'ultimo progetto sviluppato rischia di trasformare un progetto in un fallimento. D'altronde non è detto che l'infrastruttura proposta da AngularJS sia necessaria per la nostra applicazione. La scelta di Angular va valutata attentamente analizzando pro e contro, compresa la curva di apprendimento.