Introduzione alle Single Page Application Abstract Introduzione al mondo delle Single Page Application attraverso DurandalJS, RequireJS, BreezeJS, QJS, etc.. Alberto Acerbis for TheDiligence - 2 dicembre 2013
Sommario Cos è una SPA... 2 DurandalJS... 3 Struttura di una SPA con DurandalJS... 3 Creazione di una pagina... 4 Creazione del ViewModel... 4 Creazione della View... 6 Il Sistema di Routing... 7 BreezeJS... 9 Caratteristiche principali... 9 EntityManager... 9 Entity... 10 Data Binding... 10 Query... 11 Salvataggio dei Dati... 11 Metadata... 12 Promise Vs Callback... 13 Function Modules in RequireJS... 16 Fonti... 18
Cos è una SPA Una SPA è un'applicazione che prevede un modello di sviluppo completamente diverso dalle classiche applicazioni web. In una SPA l'applicazione è suddivisa in due layer principali con responsabilità ben precise: il client e il server. Il client ha la responsabilità di mostrare i dati all'utente e di permettere a questo di interagire con i dati stessi. Il server ha il compito di fornire le API di accesso ai dati che sono accedute dal client sia per leggere che per scrivere. Il layer client, e questa è la grande novità, è sviluppato completamente in JavaScript, HTML e CSS e quindi gira interamente sul browser. Il layer server può essere invece sviluppato utilizzando qualunque piattaforma server (Web API, ASP.NET MVC, ma anche servizi basati su Java o PHP o altro ancora). Client e server comunicano sfruttando normali richieste AJAX (in genere tramite il protocollo REST). Sviluppare una SPA da zero significa doversi occupare di diversi aspetti come il binding dei dati all interfaccia, e di questo ne abbiamo parlato al primo incontro introducendo KnockoutJS, la navigazione tra le pagine, il download dei file JavaScript e HTML necessari, la compatibilità tra i browser. Tutto questo richiede un approfondita conoscenza di JavaScript, oltre che un buon monte ore di tempo disponibile. La standardizzazione, e la continua diffusione di JavaScript, hanno fortunatamente permesso la creazione di framework che si occupano di gestire la maggior parte delle complessità infrastrutturali di un SPA lasciando a noi il compito di scrivere il codice di business. Di questi framework ne esistono ormai diversi, uno di questi, AngularJS, lo abbiamo visto nella presentazione di Gianni, oggi ci occuperemo di DurandalJS.
DurandalJS DurandalJS è un framework sviluppato dal team Caliburn.Micro, tanto per intenderci gli stessi di MVVM. La potenza di questo framework consiste nel fatto che il team non ho reinventato nulla, come cita il sito ufficiale, non hanno reinventato la ruota da zero, bensì ha riutilizzato framework già largamente diffusi sul web, come KnockoutJS, RequireJS e jquery e li ha uniti sotto un unico cappello. 1. JQuery viene sfruttato per le promise e per la manipolazione del DOM. 2. KnockoutJS per l implementazione del pattern MVVM. 3. RequireJS per la modularizzazione del codice. Di suo, DurandalJS, implementa un meccanismo di navigazione e un servizio di messaggistica, oltre che un sistema di routing che mappi gli url alle relative view, un meccanismo di popup, uno splash screen e altri aspetti più o meno secondari. Struttura di una SPA con DurandalJS Il template di DurandalJS nell ambiente di Visual Studio genera una applicazione ASP.NET MVC classica a cui aggiunge HTML, CSS e JavaScript, oltre a classi lato server e lato client. Dal punto di vista del server genera un controller, una view e una classe che genera un bundle. Il controller, con l originale nome di DurandalController, ha un solo metodo (index) che non fa altro che richiamare la view all interno della quale è ospitata la SPA. La view è estremamente semplice <body> <div id="applicationhost"> @RenderPage("_splash.cshtml") </div> @Scripts.Render("~/scripts/vendor") <script src="/app/durandal/amd/require.js" data-main="app/main"></script> </body>
Nella view è dichiarato un tag div all interno del quale verranno visualizzate le pagine dell applicazione, lo script del bundle generato dal template e infine il tag script responsabile dell avvio dell applicazione. Per quanto riguarda il bundle c è poco da aggiungere, la classe generata dal template non fa altro che prendere i file JavaScript necessari al template stesso e includerli nel bundle scripts/vendor.js che è poi quello utilizzato dalla view vista nel codice precedente. Da sottolineare che nel bundle non vi è presenza dei file JavaScript di Durandal questi infatti vengono caricati all occorrenza tramite Require.js. Vedremo in seguito l importanza di questa libreria. Molto più interessante è osservare cosa è successo lato client. I file necessari a Durandal sono inclusi un due cartelle App e Scripts. Nella cartella Scripts sono contenuti i file a cui Durandal si appoggia (RequireJS, KnockoutJS); i file JS strettamente appartenenti a Durandal si trovano in una sottocartella che prende il nome di Durandal. Questa cartella si può trovare nella cartella Scripts, nel caso del template Durandal, nella cartella App nel caso del template HotTowel, di John Papa, che è un implementazione di Durandal. Nella cartella App c è il file che viene lanciato al caricamento della SPA (main.js) e poi ci sono le View, che non sono altro che pagine HTML ridotte, con i relativi viewmodels, contenuti nella cartella ViewModels. Anche in questo caso ci sono alcune piccole differenza fra il template Durandal ed il template HotTowel, il secondo risulta avere una struttura un poco più ordinata e logica a mio avviso. Il nostro codice andrà a popolare sostanzialmente queste due cartelle. Creazione di una pagina La creazione di una pagina si articola in tre azioni 1. Creazione del ViewModel 2. Creazione della View 3. Aggiunta al sistema di routing Creazione del ViewModel Si tratta del file JavaScript contenente la logica della pagina. In esso vengono invocati i servizi per recuperare e/o modificare i dati. Il ViewModel espone anche le proprietà che compongono i dati che successivamente vengono collegati sulla View; in genere si tratta di observable o observabearray di Knockout in modo da poter sfruttare pienamente il relativo meccanismo di routing di quest ultimo.
define(['services/datacontext'], function (datacontext) { var coaches = ko.observablearray(); var activate = function () { // go get local data, if we have it return datacontext.getcoachpartials(speakers); ; var refresh = function () { return datacontext.getcoachpartials(speakers, true); ; var vm = { activate: activate, coaches: coaches, title: 'Coaches', refresh: refresh ; ); return vm; Dall esempio di codice possiamo vedere che un ViewModel non è altro che un modulo creato tramite la funzione define di RequireJS. Questa funzione accetta in input una lista di stringhe che rappresentano il riferimento a altri file JavaScript che non sono altro che moduli caricati sempre da RequireJS e sono quelli da cui il nostro ViewModel dipende. L ultimo parametro della funzione define è un callback che viene invocato quando RequireJS finisce di scaricare i file. Ad ogni parametro del callback corrisponde un istanza delle dipendenze specificate prima. Il callback deve restituire un oggetto che è quello che poi viene collegato alla View. Questo oggetto, come anticipato, può contenere sia proprietà che metodi che possono quindi essere invocati dalla View tramite la sintassi di KnockoutJS. Ci sono poi una serie di metodi opzionali che, se presenti, vengono invocati dal motore di DurandalJS, i più importanti sono 1. canactivate: invocato per specificare se la pagina può essere aperta
2. activate: invocato quando si naviga verso la pagina legata al ViewModel 3. candeactivate: invocato per specificare se si può abbandonare la pagina 4. deactivate: invocato quando si esce dalla pagina legata al ViewModel. E importante sottolineare che i metodi canactivae e candeactivate possono restituire un boolean o una promise, che a sua volta restituisce un boolean una volta risolta, mentri i metodi activate e deactivate possono o no restituire nulla, oppure una promise. Ovviamente, in accordo con i principi della programmazione asincrona, per tutti i metodi, nel caso venga restituita una promise, DurandalJS attende la risoluzione della stessa prima di proseguire nella pipeline. Fra i metodi elencati quello che praticamente è sempre presente è activate in quanto è il responsabile dei dai che vengono inizialmente visualizzati sulla View. Il file ViewModel, nel nostro caso coaches.js, dev essere salvato nella cartella ViewModels; deve perché, come vedremo, il sistema di routing di DurandalJS prevede questa disposizione dei file. Creazione della View La View è un file HTML, senza i tag di inizializzazione. Ricordiamoci che stiamo sviluppando una SPA, quindi tutta l applicazione è contenuta all interno di una singola pagina, perciò non avrebbe senso continuare a definire nuove pagine che SPA sarebbe!?!?! La View contiene il markup necessario a mostrare all utente i dati esposti dal ViewModel e si trova, fisicamente, nella cartella App/Views. L associazione tra la View ed il ViewModel viene risolta automaticamente da DurandalJS. Innanzitutto Durandal sfrutta il nome dei file per risolvere questa associazione, quindi i file che hanno lo stesso nome che si trovano nelle cartelle Views e ViewModels, vengono automaticamente associati fra loro. In seguito, grazie a KnockoutJS, Durandal associa i dati del ViewModel alla View, quindi è chiaro che noi dobbiamo rispettare la sintassi di KnockoutJS per decidere come visualizzare i nostri dati all utente <section class="view"> <header> <a class="btn btn-info btn-force-refresh pull-right" data-bind="click: refresh" href="#"><i class="icon-refresh"></i> Refresh</a> <h3 class="page-title" data-bind="text: title"></h3> <div class="article-counter"> <address data-bind="text: speakers().length"></address> <address>found</address>
</div> </header> <section class="view-list" data-bind="foreach: coaches"> <article class="article-container-full-width"> <div> <img data-bind="attr: { src: imagename " class="img-polaroid"/> <address data-bind="text: firstname"></address> <address data-bind="text: lastname"></address> </div> </article> </section> </section> Come si può vedere il codice delle View è estremamente semplice. Non abbiamo fatto altro che sfruttare Knockout per legare le proprietà del ViewMode agli oggetti della View e viceversa Le complessità ovviamente possono aumentare con l aumentare della complessità dell interfaccia, ma KnockoutJS mette a disposizione diversi meccanismi per semplificare lo sviluppo dell interfaccia, primo fra tutti il meccanismo dei custom binding dove i binding di base non siano applicabili; ovviamente l obiettivo è mantenere il codice delle interfacce il più pulito possibile. Il Sistema di Routing Di cosa stiamo parlando? Del modulo di DurandalJS che si occupa di trasformare un url in una richiesta a una specifica associazione View/ViewModels nella nostra SPA. Nel template originale la configurazione del routing è contenuta nel file shell.js, io preferisco creare un file separato, in genere config.js, dove specificare i parametri di configurazione della mia SPA. In ogni caso, al di là delle scelte personali di ognuno di noi, vediamo come si configura questo modulo var routes = [{ url: 'sessions', moduleid: 'viewmodels/sessions', name: 'Sessions', visible: true,
caption: 'Sessions', settings: { caption: '<i class="icon-book"></i> Sessions', { url: 'coaches', moduleid: 'viewmodels/coaches', name: 'Coaches', caption: 'Coaches', visible: true, settings: { caption: '<i class="icon-user"></i> Coaches', { url: 'sessiondetail/:id', moduleid: 'viewmodels/sessiondetail', name: 'Edit Session', caption: 'Edit Session', visible: false, { url: 'sessionadd', moduleid: 'viewmodels/sessionadd', name: 'Add Session', visible: false, caption: 'Add Session', settings: { admin: true, caption: '<i class="icon-plus"></i> Add Session' ];
BreezeJS Breeze è una libreria JavaScript che consente di gestire i dati nelle applicazioni Rich Client. L obiettivo principale di Breeze è quello di permettere la condivisione dei dati fra una pagina ed un altra senza necessariamente passare sempre dal server, e la navigabilità in essi, ossia fornire oggetti grafici complessi. Caratteristiche principali Creare oggetti lato client che rispecchiano il modello lato server. Breeze crea questi oggetti dinamicamente e lega le proprietà di questi oggetti alla UI della nostra applicazione (KnockuotJS nel nostro caso). In questo modo ogni oggetto di Breeze sa quando e quale dato è cambiato al suo interno. Permette la scrittura di Query in JavaScript, con tutte le fetures del caso, come ordinamento, filtro, raggruppamento, etc. Implementa OData (Open Data Protocol) in modo da poter espandere il risultato di una query con entità correlate. Permette di salvare un oggetto, o un gruppo di più entità, con una sola transizione. Crea una cache dei dai lato Client riducendo drasticamente la necessità di continui round-trip clientserver per recuperare dati già presenti lato Client alla transizione da una pagina ad un altra. Permette di estendere il modello con metodi, proprietà ed eventi personalizzati, è opensource cos altro volete? EntityManager E l oggetto che permette, lato Client, di accedere ai dati e crearne una cache Breeze si crea una sua struttura di Metadati, un file json che ricava con l Action Metadata nel controller che si crea, per poter navigare all interno della struttura dei dati che preleva Tipicamente si recuperano i dati da un database, o da un qualsiasi servizio di persistenza remoto, tramite una query eseguita con EntityManager; si presentano questi dati all utente tramite i meccanismi messi a disposizione dalle View, KnockouJS è uno di questi; si abilita la possibilità di manipolare questi dati con tutte le operazioni CRUD possibili e periodicamente si salvano le modifiche in sospeso con una sola transizione verso il database. EntityManager è sia un gateway per il servizio di persistenza dei dati che una cache delle entità su cui si intende lavorare lato Client. Gli oggetti interrogati e salvati sono entità mantenute nella Cache di EntityManager; queste entità possono entrare nel nostro EntityManager a seguito di una query, oppure a seguito di un operazione di inserimento lato client, o da un altro EntityManager della nostra App.
Queste entità liberano la cache, escono dal nostro EntityManager a seguito di un intervento da parte nostra, oppure perché contrassegnate come cancellate dopo un operazione di persistenza dei dati. La cache di EntityManager è interrogabile allo stesso modo in cui si interrogano i dati sul server remoto, con la sola unica grande differenza che è locale, quindi nessun round-trip Client-Server! Entity Un entità rappresenta un oggetto significativo nel modello della nostra applicazione. Potrebbe trattarsi di semplici dati, di relazioni fra entità (Cliente e relativi Ordini), di regole (una proprietà richiesta per convalidare il dato). Un entità in Breeze è un oggetto con le proprietà dei dati e le proprietà di navigazione che restituiscono entità correlate. Oltre alle normali proprietà relative ai dati, stringhe numeri, valori in genere, sono presente proprietà di navigazione fra le entità correlate nella cache di EntityManager. L esempio classico è costituito dall anagrafica Cliente con i relativi Ordini. Un entità Breeze è costituita anche da un suo kernel, una entity-ness, che rappresenta il suo entityaspect che serve al motore di Breeze per gestire l entità stessa. E possibile intervenire su questo kernel e influenzare il comportamento di Breeze nel manipolare l Entity stessa (a vostro rischio e pericolo). L aspetto più importante di una Entity è il suo EntityState. Questa proprietà indica se si tratta di un nuovo oggetto che è stato aggiunto, oppure se si tratta di un oggetto già presente ma che è stato modificato. E possibile esaminare i valori originali di un oggetto preesistente e ripristinare l entità al suo stato originale chiamando l istruzione entityaspect.rejectchanges(). Data Binding Breeze si basa principalmente sulle proprietà di Binding per intercettare le modifiche apportare lato client alle entità, il che significa che le proprietà che si vogliono trattare devono essere di tipo observable. Le proprietà in Breeze ovviamente lo sono. In ambito.net, lo sappiamo, questo meccanismo è garantito dall evento PropertyChanged, ma in JavaScript non vi è alcun meccanismo standard che implementi questa funzionalità. Ogni framework adotta la propria implementazione (KnockuotJS, AngularJS, BackboneJS, etc.). Breeze è in grado di lavorare con tutti questi framework, lasciando a noi la libertà di scegliere quello che più ci aggrada.
Query E possibile recuperare i dati da un server remoto componendo una query nella sintassi indicata da Breeze ed eseguendola attraverso un oggetto EntityManager var query = breeze.entityquery().from("coaches").where("name", "startswith", "A").orderBy("Name"); var manager = new breeze.entitymanager(servicename); manager.executequery(query).then(querysucceeded).fail(queryfailed); La prima cosa che possiamo notare è che si tratta di una chiamata asincrona, il metodo executequery restituisce una promise e invoca il metodo querysucceded nel caso l operazione vada a buon fine, oppure il metodo queryfailed in caso contrario. Breeze risolve questa query dentro con una richiesta HTTP GET, tipicamente si tratta di un servizio WebAPI con endpoint opportunamente configurato verso il nostro servizio di persistenza dei dati (SQL Server, MongoDB,..). La sintassi O-Data della query è supportata da WebAPI, ma anche da WCF e altri servizi non necessariamente.net compatibili. Ovviamente è possibile scrivere anche query complesse, sempre sfruttando la sintassi indicata da Breeze Salvataggio dei Dati Breeze aggiunge, modifica, elimina le entity nella cache di un EntityManager, mai direttamente sulla fonte dati remota. Tutto quello che fa è modificare l EntityState di una Entity. Tutte le modifiche vengono conservate e gestite nella cache sino alla chiamata del metodo SaveChanges() del nostro EntityManager.
manager.savechanges().then(savesucceeded).fail(savefailed); Ancora una volta possiamo notare che si tratta di un metodo asincrono che restituisce una promise, nel modo del tutto equivalente a quanto visto in precedenza per il recupero dei dati attraverso l esecuzione di una query. Ho già detto in precedenza che Breeze è in grado di persistere dati di una singola entità, o un batch di più entità fra loro relazionate, tutto in un unica transizione, consentendo un notevole risparmio di tempo ed un inutile round trip. Metadata Il Metadata di Breeze descrive i tipi di entità, e le relative relazioni fra di esse, in un modello di dati. Breeze necessita di questi metadati per comunicare con il servizio di persistenza durante l esecuzione di query. Si tratta di un insieme di informazioni che Breeze richiede autonomamente al server e che vengono restituite in formato json puro. Grazie a questi metadati creare un nuovo oggetto all interno di un entità diventa un operazione estremamente semplice con Breeze var newcoach = manager.createentity('coach', {CoachName='New Hero');
Promise Vs Callback Chiunque abbia scritto un minimo di codice JavaScript avrà scritto qualcosa di simile function getarticoli(callback, errorback){ $.ajax({ ); url: "/mywebapp/allspares.json", datatype: "json", success: function(data) {, // Sfrutto la callback per gestire la risposta della chiamamta ajax if (data.spares) { // OK: Abbiamo recuperato i dati, quindi possiamo invocare la callback if (typeof callback == "function" { callback(data.spares); else if (typeof errorback == "function") { errorback(new Error("Nessun Articolo Trovato"); error: function(jqxhr, textstatus, errorthrown) if (typeof errorback == "function") { errorback(errorthrown); La funzione getarticoli() dell esempio non ritorna nessun valore. L uso delle callback ci obbliga a scrivere codice strettamente accoppiato; dobbiamo conoscere esattamente dove inserire la chiamata alla callback per sortire l effetto desiderato e questo ci preclude la strada al riutilizzo del codice che stiamo scrivendo, cosa che a noi programmatori sfaticati e pigri non piace affatto! Per nostra fortuna le nuove librerie JavaScript ci offrono un alternativa alle callback, ovvero le promise, una promessa.
Una promise è un oggetto che rappresenta il valore di un azione asincrona, una cambiale per informazioni. Lo strumento messo a disposizione da jquery per implementare il meccanismo delle promise è l oggetto Deferred. L esempio riportato sopra, con l uso delle promise diventa function getarticoli(){ // Creo un'istanza dell'oggetto Deferred var deferred = new $.Deferred(); // Non faccio altro che invocare la mia funzione tramite Ajax... esattamente come prima $.ajax({ url: "/mywebapp/allspares.json", datatype: "json", success: function(data) { if (data.spares) { deferred.resolve(data.spares); else { deferred.reject(new Error("Nessun Articolo Trovato"));, error: function(jqxhr, textstatus, errorthrown) { deferred.reject(errorthrown); ); // Restituisco la promessa che rappresenta l'anagrafica articoli return deferred.promise(); La differenza fondamentale rispetto al primo approccio è la presenza di un valore di ritorno, cosa assolutamente non prevista nella versione basata sulle callback.
L oggetto che viene restituito è la cambiale, ovvero la promessa che il nostro utente potrà utilizzare per ottenere le informazioni che appunto gli abbiamo promesso, non appena queste saranno disponibili. La Deferred è il meccanismo con cui gestisco la promessa; posso creare una promessa che poi restituisco al mio utente, proprio come se lavorassi in maniera asincrona. Sarà l utente, avendo ora a disposizione questo oggetto, a collegare ad esso una callback per accedere ai valori richiesti: getarticoli().then (function (spare) { for (var i = 0, l = spares.length, i <l, i + +) { //... code code code and code more ); Si potrebbe obiettare dicendo che alla fine è ancora una callback quella che mi serve. Certamente! Ma a invocare e consumare la callback ora è il codice che ha invocato il tutto, la mia funzione non è assolutamente accoppiata con esso; in questo modo abbiamo astratto la funzione di estrazione articoli e possiamo riutilizzarla ognivolta che ci tornerà comodo, decidendo di volta in volta quale funzione attaccare tramite callback. La libreria che ci permette di gestire le promise è Q.js
Function Modules in RequireJS La caratteristica principale di RequireJS è la possibilità di definire un modulo come funzione. Se in un primo momento questo può sembrare una caratteristica piuttosto semplice, come in effetti lo è, ci sono alcuni aspetti pratici in cui tornare una funzione come modulo può rivelarsi piuttosto utile. L implementazione di una funzione come modulo è piuttosto semplice // random.js define(function(){ ); return function(min, max){ return Math.floor((Math.random()*max)+min); Dopo di che il modulo può essere richiamato nel modo seguente: require(['random'], function(random){ console.log(random(1,10)): ); Un modulo può essere utilizzato come mezzo conveniente per creare uno specifico tipo di oggetto su una entità in base a determinate condizioni. Prendiamo questo semplice esempio che, dato uno specifico ruolo, restituisce un implementazione corrispondente della View // app/views/editors/editorfactory.js define(function(require){ var editors = { 'admin' : require('editors/admineditorview'), 'user' : require('editors/usereditorview'), 'guest' : require('editors/guesteditorview'), // additional implementations... ; return function(role){ try {
); return new editors[role]; catch(error) { throw new Error('Unknown Role Specified.'); Il ViewModels può semplicemente invocare il modulo per recuperare l appropriata implementazione della View in base al ruolo specificato define(function(require){ var factory = require('editors/editorfactory'); return Backbone.View.extend({ render: function(){ //... view rendering this.editor = factory(this.model.get('role')); this.editor.render(); ); ); La definizione di funzioni come modulo semplifica notevolmente l implementazione del codice, consentendo una scrittura del codice più semplice, facilmente testabile e manutenibile.
Fonti http://www.html5italia.com/articoli/html5/creare-single-page-application-aspnet-mvc-durandaljs.aspx http://www.randomthink.net/blog/2012/10/callbacks-and-promises/ http://blog.jcoglan.com/2013/03/30/callbacks-are-imperative-promises-are-functional-nodes-biggestmissed-opportunity/ http://www.ericfeminella.com/blog/2013/01/06/function-modules-in-requirejs/