Questo post fa parte di una serie dedicata ad Aurelia e ASP.NET Core.
Al termine dell'ultimo post ci siamo lasciati con tre domande cui cercheremo di rispondere in tre post ravvicinati nei prossimi giorni.
Cos'è la strana sintassi che abbiamo usato nel file app.js?
Nel primo post di questa serie avevamo accennato al fatto che Aurelia è scritto interamente in ES2016, l'ultimissima versione delle specifiche JavaScript che va ulteriormente ad estendere il già innovativo ES2015 (per lungo tempo altrimenti noto come ES6).
Descrivere, anche in modo sommario, tutte le nuove caratteristiche di ES2015/6 ci farebbe deviare troppo dalla nostra rotta. Userò quindi il "solito" approccio di limitarmi a descrivere quello che ci è realmente utile al momento, lasciando eventuali ulteriori approfondimenti ad un prossimo post.
Classi
ES2015 introduce in JavaScript il concetto di classe. Proprio come in C# o in altri linguaggi ad oggetti, una classe è una matrice per creare oggetti: ogni oggetto al momento della creazione sarà fornito degli attributi (proprietà) e dei comportamenti (metodi) definiti nella classe.
Il concetto di oggetto non è ovviamente nuovo in JavaScript ma per via della complessità nella sua gestione a 360 gradi e dell'approccio radicalmente diverso dagli altri linguaggi ad oggetti, le nuove specifiche hanno aggiunto un po' di "syntactic sugar" per mezzo della nuova keyword class. Creare un oggetto in JavaScript è quindi ora molto simile alla sua controparte C#: si definisce una classe e si istanzia l'oggetto sempre usando new. La definizione delle proprietà della classe avviene direttamente all'interno del costruttore mentre i metodi possono essere aggiunti al corpo (FYI ES2016 ha ulteriormente esteso questo nuovo costrutto dando la possibilità di definire anche le proprietà nel corpo della classe, in maniera più simile a quanto facciamo con C#):
Con il nuovo costrutto, l'ereditarietà viene gestita per mezzo della keyword specifica extends:
Ho già avuto modo di dire che si tratta solo di "syntactic sugar". Dietro le quindi il funzionamento è ancora quello di ES5, basato su funzioni e prototype. Ma di questo parleremo magari meglio in un post dedicato, fuori da questa serie su Aurelia. In questo contesto ci basti sapere che questa sarà la sintassi che useremo ogni volta che dovremo definire un ViewModel da associare ad una View. Ma il pur banale ViewModel che abbiamo costruito presentava un'altra keyword "strana": export. Di che si tratta?
Moduli
Da sempre JavaScript soffre di problemi di scarsa organizzazione del codice e mancanza di controllo sulla visibilità dello stesso. Per farla breve:
- Tutto ciò che viene definito (funzioni, variabili, ecc.) diventa un attributo dell'oggetto globale window il che, oltre a generare una certa confusione, espone le applicazioni a conflitti sui nomi quando si usano librerie provenienti da fonti diverse (che come sappiamo in JavaScript è la norma);
- Come se non bastasse, tutto il codice scritto è esposto pubblicamente, non c'è modo di definire logiche interne ad un oggetto come facciamo ad esempio con i metodi privati in C#, e la dinamicità del linguaggio (inclusa la possibilità di sostituire il corpo di una funzione con codice completamente diverso a runtime con una semplice assegnazione) non fa che peggiorare le cose;
- Spesso parti di codice definite in file o librerie diverse dipendono l'una dall'altra e non è sempre immediato garantire che il file che contiene una certa funzione venga caricato prima di quello che la utilizza, causando in qualche occasione eccezioni non previste.
Ovviamente la comunità di sviluppatori JavaScript non è stata semplicemente ferma a subire queste lacune del linguaggio ma ha piuttosto cercato forme alternative per ottenere il risultato desiderato (e come ormai dovrebbe essere chiaro in un linguaggio dinamico come JavaScript c'è quasi sempre un modo, magari non proprio ortodosso, per raggiungere il proprio scopo).
Dopo un primo tentativo di mitigazione tramite le cosidette Immediately-Invoked Function Expression (IIFE), una seconda risposta è arrivata dalle specifiche CommonJS e da quelle AMD (Asyncronous Module Definition) su cui si basa RequireJS. Alla base di questo nuovo approccio ci sono i concetti di modulo come unità indipendente e quelli di esportazione e importazione del contenuto dello stesso per definire i livelli di visibilità e le dipendenze tra i moduli.
ES2015 contribuisce a standardizzare quelle specifiche attraverso l'introduzione di due nuove keyword: export e import appunto. Ogni file JavaScript costruito secondo le specifiche costituisce un modulo a sé stante i cui elementi interni (variabili, funzioni, classi) vivono in isolamento rispetto agli altri moduli e al contesto globale. Gli elementi del modulo (uno o più) che si vogliono rendere pubblici devono essere esplicitamente marcati con la keyword export, come abbiamo fatto nel post precedente nel file app.js, e importati nel modulo che ne vuole usufruire (come si vede dalle "ondine" rosse, Visual Studio 2015 ad oggi non digerisce molto bene la sintassi ES2015 nel file js):
Aurelia si occupa di caricare i moduli che compongono i ViewModel automaticamente quando sia necessario il render della corrispettiva View, quindi tipicamente non avremo bisogno di importare i ViewModel nella nostra applicazione. Avremo invece bisogno di importare i moduli JavaScript che useremo ad esempio per incapsulare il codice di interfacciamento con i nostri servizi di back-end.
Promise
C'è un ultimo aspetto di ES2015 di cui dobbiamo parlare anche se non l'abbiamo incontrato nel primo semplicissimo ViewModel. Aurelia è infatti fortemente basato sulla programmazione asincrona. La quasi totalità delle funzioni del framework è progettata per ritornare immediatamente il controllo al chiamante mentre prosegue l'esecuzione delle operazioni, salvo poi notificare il completamento delle stesse a tempo debito.
Il concetto di programmazione asincrona è ben noto agli sviluppatori web per via della latenza intrinseca nell'architettura client/server basata su HTTP. Ogni volta che comunichiamo con il server per ottenere delle informazioni, non possiamo fare assunzioni sui tempi di risposta e quindi, se non vogliamo bloccare completamente l'esecuzione dell'applicazione, è buona norma tenere conto di queste limitazioni e progettarla di conseguenza.
Il modo "tradizionale" di gestire la programmazione asincrona in JavaScript (e non solo) era basato sul meccanismo delle callback. Una funzione che doveva eseguire operazioni potenzialmente molto lunghe prevedeva tra i propri parametri una funzione da chiamare al termine delle operazioni. In questo modo era possibile ritornare immediatamente il controllo al chiamante ed eseguire poi il codice in risposta al completamento nella funzione di callback.
Non mi dilungherò sui tanti problemi che questo comporta. Trovate tonnellate di documentazione in rete (tra cui ad esempio le slide della mia sessione a .NET Campus). In ogni caso per superare tali limitazioni già da diversi anni si è diffuso l'utilizzo delle promise come alternativa alle callback.
Anche in questo caso infatti, come per le classi e i moduli, ES2015 arriva a standardizzare un approccio già in voga tra gli sviluppatori, con diverse librerie che fornivano un'implementazione delle specifiche Promises/A+ come ad esempio Q.
Una promise è in definitiva (e con un'innocente semplificazione) un oggetto che rappresenta un valore che al momento non è noto ma che lo sarà prima o poi nel futuro. Come interagiamo con tale oggetto? Fondamentalmente per mezzo del suo metodo then – che da specifiche ogni promise deve obbligatoriamente implementare– che accetta come parametri le due funzioni che saranno invocate rispettivamente quando la promise viene risolta positivamente o quando viene viceversa rigettata (ad esempio per un timeout).
Molto spesso del corso dei prossimi post ci imbatteremo quindi in codice come il seguente:
Due cose interessanti vanno aggiunte sul funzionamento di questo meccanismo: intanto il metodo then ritorna a sua volta una promise usando il valore di ritorno della nostra funzione per risolvere la stessa o intercettando un'eventuale eccezione lanciata dal nostro codice per rigettarla. Questo vuol dire che possiamo concatenare chiamate al metodo then con una sintassi molto più chiara e interpretabile della "pyramid of doom" caratteristica delle callback. In secondo luogo, essendo oggetti, le promise possono essere sia memorizzate in una variabile che passate da una funzione all'altra, aprendo la porta a scenari molto interessanti che avremo modo di scoprire nei prossimi post:
E TypeScript in tutto ciò?
Aurelia supporta non solo ES5 e ES2015/6 ma anche TypeScript. Ciò vuol dire che possiamo scrivere la nostra applicazione nel modo che preferiamo, anche se il consiglio è ovviamente di evitare ES5 e orientarsi per una delle due opzioni più "moderne".
Nel corso di questa serie userò principalmente ES2015, ma non escludo di fare qualche esperimento anche con TypeScript più avanti. La differenza in realtà non è più così rilevante come era tra ES5 e TypeScript.
Le aggiunte più interessanti di quest'ultimo rispetto a ES2015 sono a mio parere la type safety (cioè la possibilità di definire quale sia il tipo di variabili, parametri e valori di ritorno), i generic (praticamente uguali anche nella sintassi a quelli di C# e molto utili in diverse situazioni) e la possibilità di definire interfacce. Tutte cose utili ma non indispensabili per il proseguo del nostro progetto.
Happy coding!