• Non ci sono risultati.

2. EDITION VISUALIZATION TECHNOLOGY

2.3 Tecnologie utilizzate e infrastruttura

2.3.1 Ambiente di sviluppo

Per la gestione dell’ambiente di sviluppo e per la realizzazione del pacchetto finale del software sono stati utilizzati i software Bower e Grunt.

Bower è un package manager per lo sviluppo Web, si occupa cioè della gestione del pacchetto finale assicurandosi che anche i componenti al suo interno siano aggiornati (o impostati a versioni specifiche necessarie).

In altre parole, installa automaticamente la versione più appropriata dei plugin, dei framework e tiene traccia delle loro dipendenze in un file

manifest, bower.json. Se più pacchetti dipendono da un altro pacchetto,

89 Bower: https://bower.io/. 90 Grunt: https://gruntjs.com/.

46

jQuery per esempio, Bower scaricherà jQuery solo una volta, questo aiuta a ridurre il numero e quindi i tempi di caricamento.

Tuttavia, come spesso accade nello sviluppo Web, questa tecnologia è stata sfruttata con successo per qualche anno, ma ad oggi risulta obsoleta e se ne sconsiglia l’uso per nuovi progetti. La ragione principale è semplice: sono state sviluppate tecnologie più avanzate ed efficienti per la gestione delle dipendenze dei nodi.92

Malgrado i browser Web offrano ancora supporto a Bower, il team di EVT 2js sta ultimando la migrazione verso un altro gestore di pacchetti,93

per garantire al software accessibilità a lungo termine.

Un altro limite di Bower è l’impossibilità di eseguire operazioni di concatenazione o compressione del codice, di conseguenza è stato necessario integrare nell’ambiente di sviluppo un plugin aggiuntivo, Grunt. Grunt è un task runner, ovvero un plugin che una volta configurato esegue compiti ripetitivi come la compressione di file JavaScript, la compilazione di file Sass, l’esecuzione dei test, la validazione del codice, etc. In altre parole, esegue operazioni concatenabili e permette di costruire un

workflow efficiente per la realizzazione di applicazioni Web.

Molte operazioni come concat per la concatenazione di file e min per la compressione dei file JavaScript, sono supportate nativamente da Grunt. Se i task necessari non sono già stati implementati, si possono creare e pubblicare plugin personalizzati per realizzare task aggiuntivi.

Grunt può essere utilizzato all’interno del proprio progetto installando npm (Node.js package manager, un gestore di pacchetti di default per l’ambiente a runtime JavaScript Node.js). La configurazione dei task personalizzati si realizza attraverso pochi semplici passaggi: (1) creare nella cartella principale un file di configurazione che attraverso un oggetto

92 In particolare, Yarn (Yet Another Resource Negotiator) è un gestore di pacchetti migliore per molti aspetti: è rapido, sicuro e affidabile. Yarn: https://yarnpkg.com/.

93 In particolare, Webpack è un gestore di pacchetti per le applicazioni JavaScript. Webpack costruisce internamente un grafico di dipendenza che mappa ogni modulo di cui il progetto ha bisogno e genera uno o più pacchetti. Webpack: https://webpack.js.org/.

47

JavaScript configura i diversi file; (2) aggiungere un file package.json in cui

vengono definite le diverse dipendenze del progetto.

Il successo di questo plugin è dovuto al suo costante aggiornamento da parte della comunità open source e dalla grande quantità di materiale informativo presente online, caratteristiche che lo designano come strumento ideale anche per lo sviluppo di EVT 2js.

2.3.2 Angular JS

Il codice di EVT 2js è scritto in linguaggio di programmazione JavaScript, sfruttando le potenzialità di un framework open source che offre una struttura modulare formata da vari componenti, ciascuno con una funzione ben precisa. AngularJS, creato nel 2009 da Miško Hevery e Adam Abrons e supportato da Google, consente di semplificare lo sviluppo e l’esecuzione di test su SPA (Single Page Applications), cioè applicazioni le cui risorse vengono caricate dinamicamente su richiesta, senza necessità di ricaricare l’intera pagina.

In altre parole, Angular è un’infrastruttura per la creazione di applicazioni composta da un insieme di funzionalità. Citando la documentazione ufficiale: “Angular è quello che HTML avrebbe dovuto essere se fosse stato progettato per sviluppare applicazioni.”94

L’infrastruttura di sviluppo si fonda su un pattern architetturale che consente la separazione delle competenze. L’approccio MVC (Model View

Controller) consente di distinguere tra i dati sottostanti l’applicazione

(Model), le modalità con le quali essi vengono presentati all’utente (View) e l’interazione tra le due parti, che viene gestita da un componente separato (Controller).

La distinzione della logica applicativa dalla presentazione garantisce l’indipendenza tra i componenti e quindi favorisce le operazioni di manutenzione dell’applicazione. Si possono effettuare cambiamenti alla

48

vista senza intervenire sul modello, ma anche avere viste differenti di uno stesso modello di dati senza necessariamente intervenire sulla vista.

In AngularJS l’approccio MVC è stato implementato in modo da sviluppare e definire la logica di controllo dell’applicazione con notevole flessibilità: la View è il DOM del documento HTML, il Controller è costituito da classi JavaScript e il Model viene salvato all’interno delle proprietà di oggetti JavaScript.

La filosofia di AngularJS aderisce alla perfezione al pattern MVC, tanto che uno dei suoi autori per sottolineare la sua estrema flessibilità lo ha definito MVW (Model View Whatever).

In verità il supporto del pattern è solo una delle sue caratteristiche vantaggiose, AngularJS consente anche di estendere l’HTML con elementi e attributi personalizzati indipendenti dal resto dell’applicazione, elementi e attributi che, se ben progettati, possono essere riutilizzati in altri progetti.

Inoltre, sfrutta un approccio del tutto client side, cioè non richiede di comunicare con il server per aggiornare i contenuti da visualizzare.

La composizione di un’applicazione utilizzando moduli diversi è ottenuta tramite il pattern dependency injection, che permette di gestire in maniera efficiente ed efficace le dipendenze tra i vari moduli. In altre parole, la dependency injection permette di ridurre il codice da scrivere.

Un’altra delle feature fondamentali riguarda l’approccio alla manipolazione dell’interfaccia utente, basato sull’interpolazione di espressioni, sull’utilizzo di direttive e sul data-binding bidirezionale (two-

way data-binding).

L’approccio dichiarativo permette di sincronizzare il modello e la vista senza particolari artifici di programmazione, in modo da riflettere nella vista ogni cambiamento avvenuto nei dati di riferimento e viceversa.95 Infine,

AngularJS offre un meccanismo di routing che permette di passare da una vista all’altra senza necessariamente ricaricare la pagina: è infatti possibile

49

mappare uno specifico URL ad una certa vista, dando all’utente l’illusione di navigare tra pagine diverse.

AngularJS è strutturato e organizzato in moduli e in componenti, che comunicano tra loro sfruttando il meccanismo di dependency-injection.

I componenti appartengono a due principali categorie di oggetti: i servizi e gli oggetti specializzati. I servizi a loro volta si distinguono in tipologie: service, factory, constant, value e provider. Gli oggetti specializzati sono: controller, direttive, filtri e animazioni.

Le prossime sottosezioni si propongono di fornire la descrizione dettagliata dei moduli e dei componenti di AngularJS utilizzati per lo sviluppo di EVT 2js.

Moduli

Nella terminologia del framework un modulo è un oggetto che ha lo scopo di definire le caratteristiche di un’applicazione in termini di configurazione, servizi, controller, direttive, etc.

I moduli sono delle collezioni di direttive, filtri e altre funzioni offerte da AngularJS per organizzare il codice e avviare l’applicazione. La definizione del modulo utilizza il metodo module() dell’oggetto globale angular, e passa una stringa che rappresenta il nome del modulo e un elenco di eventuali dipendenze:

angular.module('nomeModulo', [d1, ..., dn]);

In questo estratto di codice angular è l’oggetto fornito dal framework che permette di dichiarare i moduli; nomeModulo indica il nome stabilito dallo sviluppatore per il modulo; [d1, ..., dn] indica l’array dei moduli da cui dipende il modulo.

La stessa sintassi viene usata anche per invocare un certo modulo all’interno dell’applicazione. Per registrare un servizio, un filtro o una

50

direttiva su un modulo, si invoca il modulo con la sintassi appena indicata e su di esso si invoca il metodo per la creazione del nuovo componente. Per indicare il punto del template HTML in cui agisce un certo modulo, ci si serve della direttiva ng-app, ponendola come attributo sull’elemento di riferimento e assegnando come valore il nome del modulo.

<html ng-app='nomeModulo'></html>

La direttiva si serve poi del modulo per avviare l’applicazione. Si consiglia di creare un modulo rispettivamente per ogni componente o funzionalità dell’applicazione e per ogni componente riutilizzabile. Inoltre è opportuno avere un modulo alla base dell’applicazione che dipenda dagli altri moduli e contenga il codice per inizializzare l’applicazione.

Controller

I controller hanno lo scopo di controllare le interazioni tra il modello dei dati e le modifiche effettuate sulla vista dall’utente. La definizione di un nuovo controller richiede la scrittura del codice:

angular.module('nomeModulo', [d1, ..., dn])

.controller('nomeController', ['$scope', function ($scope) { $scope.greeting = 'Hello world!';

}]);

Essendo associati a un elemento HTML hanno accesso al rispettivo scope e vengono utilizzati per inizializzare lo stato dell’oggetto o per aggiungere comportamento allo scope. Per associare un controller a un nodo HTML si utilizza la proprietà controller di una direttiva personalizzata o la direttiva ng-controller, nel punto in cui si desidera visualizzarne il contenuto:

51

All’interno di una direttiva, alla creazione di un nuovo oggetto controller, viene creato un nuovo scope figlio che viene passato al controller come parametro. Se il controller viene aggiunto alla direttiva con la sintassi controller as allora l’istanza del controller sarà assegnata a una proprietà del nuovo scope.

Scope

La funzione che definisce il controller ha il parametro $scope. Questo viene passato dal framework al controller ed è un oggetto condiviso con la view. Si tratta di un oggetto che fa riferimento al modello dell’applicazione e che sostituisce il contesto di esecuzione per le espressioni. Gli scope sono organizzati in una struttura gerarchica modellata sulla struttura del DOM dell’applicazione. Gli scope figli ereditano le proprietà dello scope padre, a meno che non siano stati creati come scope isolati.

Gli scope consentono la sincronizzazione di view e controller con il modello dei dati grazie ai metodi che possiedono per osservare se nel modello o nella vista avvengono dei cambiamenti e ai metodi per propagare i cambiamenti registrati in un punto del sistema nelle altre parti.

Controller senza scope

È possibile definire un controller senza ricorrere allo scope. In questo caso è lo stesso controller a veicolare il modello dati e le funzionalità per la view:

.controller('nomeController', function () { this.utente = { nome: '...', cognome: '...' }; this.saluta = function () {

return ...; }

});

52

Filtri

I filtri possono essere usati nei template, nei controller o nei servizi, sono componenti con il compito di formattare o applicare un’elaborazione al risultato di un’espressione da mostrare nella vista. Un’espressione può essere formattata da più filtri concatenati, in modo da applicare contemporaneamente più operazioni che modificano l’aspetto dell’espressione. Il loro utilizzo all’interno della view è basato sull’operatore

pipe(|):

{{'Hello Angular!' | uppercase}}

In questo caso la stringa viene elaborata dal filtro uppercase che trasforma tutto in maiuscolo. Nell’interfaccia la visualizzazione sarà: “HELLO ANGULAR!”. Un filtro inoltre può avere uno o più argomenti, che servono a definire dei parametri per l’operazione eseguita dal filtro. Il filtro number, accetta come argomento il numero di cifre decimali con cui mostrare un numero:

{{ 1234 | number: 2 | currency:'$' }}

L’estratto di codice mostra number e currency che sono due dei diversi filtri predefiniti forniti da AngularJS, il filtro currency che aggiunge al un numero concatenato number il simbolo passato come argomento. Nell’interfaccia la visualizzazione sarà: “$1.234,00”.

È possibile creare dei filtri personalizzati tramite la seguente sintassi:

angular.module('nomeModulo')

.filter('nomeFiltro', function () { return function (input, ...) { /* [...] */

53 return output;

} });

Nell’estratto di codice nomeFiltro è il nome assegnato al filtro. La funzione utilizzata per registrare un nuovo filtro deve essere pura, ovvero

stateless e idempotente: l’input non deve essere modificato dalla funzione

stessa e non deve causare altri effetti collaterali (idempotenza); se viene eseguita più volte utilizzando lo stesso input, l’output generato sarà sempre lo stesso (stateless). È possibile definire anche filtri a stateful, ma Angular non garantisce l’ottimizzazione della loro esecuzione.

Servizi

I servizi sono componenti che offrono funzionalità indipendenti dall’interfaccia utente per implementare la logica dell’applicazione. Si tratta cioè di funzionalità che si occupano di elaborare e/o recuperare i dati da visualizzare sulle view tramite i controller, con lo scopo di rendere accessibili le funzionalità anche agli altri componenti dell’applicazione (controller, filtri, direttive e altri servizi).

In altri termini, se nella nostra applicazione è necessaria una funzione con una finalità ben precisa e tale funzione è utilizzata da più componenti, il modo migliore per gestire questa necessità è implementare un servizio.

Da un punto di vista tecnico i servizi AngularJS sono oggetti

singleton96, quindi di essi esiste una sola istanza accessibile dagli altri

componenti e possono essere utilizzati per la comunicazione tra componenti differenti grazie al meccanismo di dependency-injection.

96 Il singleton è un design pattern creazionale che ha lo scopo di garantire che di una determinata classe venga creata una e una sola istanza, e di fornire un punto di accesso globale a tale istanza. Wikipedia, voce Singleton:

54

Un controller può memorizzare i dati in un service ed un eventuale nuovo controller può scaricare tali dati dal servizio senza richiederli nuovamente al server o avviare funzioni di parsing già eseguite.

Le due principali modalità di creazione di un servizio utilizzano i metodi service() e factory(). La differenza fra le due modalità è abbastanza sottile e apprezzabile quando abbiamo esigenze particolari. La prima modalità permette di registrare un servizio più complesso come un costruttore di funzione, invocato tramite la parola chiave new. Il metodo service() ci fornisce un’istanza della funzione associata al servizio e ha la sintassi:

angular.module('nomeModulo', [])

.service('nomeServizio', function ([d1, ..., dn]) { [...]

});

Nell’estratto di codice nomeServizio è il nome identificativo del service, function([d1, ..., dn]) è il costruttore della funzione che verrà istanziata, e [d1, ..., dn] è un array di zero o più dipendenze. Il metodo factory() ci fornisce il valore restituito dall’esecuzione della funzione associata al servizio:

angular.module('nomeModulo', [])

.factory('nomeFactory', function ([d1, ..., dn]) { var myFactory = { [...] }

return myFactory; });

L’istanza di oggetto che rappresenta il servizio e restituita alla fine della funzione, indicata dalla variabile myFactory.

55

La scelta tra i due tipi di servizi service o factory dipende dal modo in cui il servizio dovrà essere utilizzato: il metodo service viene preferito quando si ha la necessità di definire il servizio come una classe, il metodo factory viene utilizzato quando si vuole definire il servizio come istanza di un oggetto e non si ha quindi necessità di invocare un costruttore.

In aggiunta a questi due metodi si possono utilizzare anche altre modalità per definire un servizio: constant, value e provider.

I primi due metodi non possono dipendere da altri componenti e vengono solitamente utilizzati per definire valori primitivi o oggetti da iniettare nei vari componenti dell’applicazione.

La differenza tra constant e value consiste nel fatto che le costanti possono essere utilizzate in fase di configurazione e non possono mai essere modificate dai vari componenti.

La sintassi da usare per il metodo costant con un valore di tipo oggetto è:

angular.module('nomeModulo', []) .constant('nomeCostante', { chiave: valore,

[...] });

La sintassi da usare per il metodo value con un valore di tipo primitivo è:

angular.module('nomeModulo', []) .value('nomeValore','stringa');

I due estratti di codice indicano: nomeCostante e nomeValore che sono i nomi univoci di riferimento.

56

Un altro metodo per definire un servizio è il provider, che si differenzia dagli in quanto è l’unico che può essere iniettato nei componenti che ne hanno bisogno nella fase iniziale di configurazione. Il provider è definito come un tipo custom che implementa il metodo $get. La parte interna di questo metodo si istanzia come service e visibile a tutti i moduli che lo iniettano. La sintassi per definire un provider è la seguente:

angular.module('nomeModulo', [])

.provider('nomeProvider', function ([d1, ..., dn]) { [...]

});

Nel codice riportato nomeProvider è il nome identificativo del provider, function([d1, ..., dn]) è una funzione che crea una nuova istanza di un servizio, e [d1, ..., dn] è un array di zero o più dipendenze.

Direttive

Le direttive sono l’unico componente abilitato a manipolare il DOM, intervenendo direttamente sull’interfaccia utente. Sono dei marcatori aggiunti a un elemento HTML, al fine di estenderne il linguaggio e modificare il comportamento di un elemento standard.

In aggiunta alle numerose direttive predefinite, riconoscibili dal prefisso ng- (ng-show, ng-class, ng-repeat, etc.), si possono definire direttive personalizzate, specificandole come elementi o attributi, come classi o commenti.

L’impiego delle direttive è consigliato per semplificare il codice HTML dell’applicazione, soprattutto nel caso in cui questo risulti ridondante. Per la sua definizione è necessario indicare un nome specifico e una funzione che restituisce un oggetto contenente le impostazioni della direttiva stessa. È necessario inoltre restringere il suo campo di utilizzo dichiarando la tipologia (elemento, attributo, classe, commento) e

57

all’occorrenza specificare un template particolare, definire le variabili di $scope e assegnare un controller per gestire la view.

Per effettuare operazioni preliminari o definire comportamenti non descrivibili solo per mezzo del template, è possibile aggiungere alla direttiva del codice JavaScript. La sintassi per creare una nuova direttiva è una funzione che restituisce un oggetto che contiene le impostazioni della direttiva stessa:

angular.module('nomeModulo', [])

.directive('nomeDirettiva', function () { return { restrict: string, priority: number, template: string, templateUrl: string, transclude: bool, scope: bool or object, controller: string, require: string,

link: function (scope, element, attrs) { [...]

},

compile: function (element, attrs, transclude) { [...]

return {

pre: function preLink(scope, element, attrs) { [...]

},

post: function postLink(scope, element, attrs) { [...]

58 }

} }; });

Analizzando questo frammento di codice possiamo notare molti elementi nuovi: nomeDirettiva indica il nome identificativo della direttiva personalizzata; restrict indica la tipologia della direttiva e quindi come essa dovrà essere invocata, esistono quattro valori possibili: (1) A, Attributo; (2) E, Elemento; (3) C, Classe; (4) M, Commento. Tali valori possono essere combinati: la direttiva può essere invocata come elemento, come attributo e come classe indicando ‘AEC’; priority indica la priorità di esecuzione da assegnare alla direttiva nel caso in cui questa venga utilizzata contemporaneamente ad un’altra in uno stesso elemento del DOM; template e templateUrl indicano il codice HTML da appendere o sostituire alla direttiva, eventualmente definito in un file separato;

transclude consente di spostare il contenuto della direttiva all’interno del

template definito, sostituendolo (se true) al tag marcato con ng-

transclude; scope consente di assegnare alla direttiva uno scope personalizzato, se il valore della proprietà è uguale a true il nuovo scope eredita le proprietà dello scope padre, se scope è uguale a un nuovo oggetto, viene creato uno scope isolato; controller indica il nome del controller da assegnare alla direttiva; controllerAs consente di assegnare l’istanza del controller a una proprietà dello scope della direttiva; require consente di indicare la dipendenza della direttiva da un’altra; link e compile permettono di dichiarare una funzione per aggiornare il DOM o registrare

listeners su determinati elementi.

La differenza tra i due consiste nel fatto che con compile la funzione viene eseguita prima della trasformazione dal template alla vista, con link la funzione viene eseguita dopo.

59

In genere la funzione compile viene utilizzata per modificare qualcosa in comune a tutte le copie, dato che a differenza di link è eseguita solo una volta.

Template

La proprietà template consente di definire le viste, come già indicato poco sopra indica il markup HTML da iniettare o sostituire alla direttiva. Se il

markup è complesso può essere scomodo definirlo in tale posizione.

Possiamo allora definirlo in un file separato e ricorrere alla proprietà templateUrl per indicare ad AngularJS la fonte da cui attingere.

I template combinano le informazioni al loro interno con quelle ricavate dal modello e dai controller. Si definiscono attraverso direttive, filtri o con un meccanismo di collegamento interpolato (interpolation

binding), che collega le espressioni marcate da parentesi graffe agli elementi

HTML. Per definire un collegamento interpolato è sufficiente adoperare la sintassi:

<elemento>{{nomeVariabile}}</elemento>

In questo caso il contenuto dell’elemento è definito attraverso la variabile nomeVariabile, associata allo scope che controlla l’elemento.