Questa è la traduzione dell'articolo Javascript MVC di Jonathan Snook, pubblicato originariamente su A List Apart il 18 Agosto 2009. La traduzione viene qui presentata con il consenso dell'editore (A List Apart Magazine) e dell'autore.
Se un tempo era un attore secondario, oggi Javascript ha guadagnato una posizione centrale sul palcoscenico. Il suo ruolo, lo spazio che occupa sui nostri server e nelle nostre attività di sviluppo continuano a crescere. E allora, come possiamo rendere il nostro Javascript più riusabile e facile da mantenere? Forse MVC può offrirci un modo per raggiungere questo risultato.
Se MVC è un termine familiare a quanti sono impegnati nello sviluppo di applicazioni sul lato back-end usando framework come Struts, Ruby on Rails e CakePHP, l'origine di MVC nello sviluppo di interfacce utente si adatta anche alle applicazioni client-side. Esaminiamo allora cos'è MVC, come possiamo usarlo per ristrutturare un progetto di esempio e consideriamo alcuni framework MVC.
Cos'è MVC?
L'acronimo è stato usato già sei volte in questo articolo e, se non lo avete sentito prima, vi starete forse chiedendo cosa significa. MVC sta per Model-View-Controller (Modello-Vista-Controllore). È un pattern architetturale che spezza un'applicazione in tre parti: i dati (Model), la presentazione dei dati all'utente (View), le azioni ricevute da qualunque interazione dell'utente (Controller).
Nel 1978, allo Xerox PARC, Trygive Reensaku ha così ricordato le origini del concetto di MVC (PDF):
"Ci sono quattro ruoli in questo paradigma dell'interazione con l'utente. L'utente Umano ha un modello mentale dell'informazione che egli sta gestendo in uno specifico momento. L'oggetto che svolge il ruolo del Model è la rappresentazione interna e invisibile che il computer ha di questa informazione. Il computer presenta differenti aspetti dell'informazione attraverso oggetti che svolgono il ruolo di View, diverse Views (viste) degli stessi oggetti possono essere contemporaneamente visibili sullo schermo di un computer. Gli oggetti che svolgono il ruolo di Controller traducono i comandi dell'utente in messaggi appropriati per gli oggetti View o Model a seconda delle necessità."
In altre parole, un utente fa qualcosa. Questo "qualcosa" viene passato ad un Controller che controlla cosa dovrebbe accadere successivamente. Spesso, il Controller richiede dati dal Model e quindi li passa alla View, che li presenta a sua volta all'utente. Ma cosa significa questa separazione per un sito o un'applicazione web?
Le basi
Un documento statico è la base di una pagina web. Ogni pagina che ci viene servita rappresenta lo stato dell'informazione sul server in quel momento. Ma non riceviamo solo dati grezzi, otteniamo invece una pagina HTML che il browser rende in tutta la sua bellezza impostata grazie ai CSS.
Anni fa, se volevamo modificare quei dati, il server doveva presentare una pagina con input e aree di testo per rendere possibili i cambiamenti. Rispedivamo allora quei cambiamenti indietro verso il server e aspettavamo fino a quando non ci veniva risposto che era tutto a posto. Richiedere una pagina completamente nuova ogni volta che volevamo apportare dei cambiamenti divenne presto qualcosa di fastidioso per l'utente, ancora di più quando egli compiva qualche errore e doveva reinserire i dati.
Il cavaliere con la sua scintillante armatura
Da quei giorni lontani, Javascript, con Ajax, è intervenuto a modificare in meglio lo scenario. Esso ci consente di aggiornare elementi della pagina e di rimandare al server le richieste dell'utente. Ancora più importante è il fatto che consente alle nostre pagine di riflettere le richieste dell'utente senza dover aspettare il server.
Ora, a questo livello dello sviluppo e dell'adozione di Javascript e Ajax, abbiamo bisogno di considerare l'idea di separare i componenti del nostro codice in base allo stile definito dal modello MVC. Questo tipo di separazione potrebbe non essere necessaria in ogni situazione e in certi casi potrebbe rendere le cose complicate senza che ce ne sia bisogno. Di fatto, però, man mano che le nostre applicazioni diventano più complesse e richiedono interazioni Javascript in molte parti del sito, separare Javascript in Model, View e Controller può produrre un codice più modulare e riutilizzabile.
Strutturare il nostro codice
Javascript è stupido. Non capisce ciò che un documento HTML sta provando a dire all'utente, oppure quello che gli utenti stanno cercando di ottenere usando una pagina. Noi sviluppatori dobbiamo dire al nostro Javascript cosa siginificano gli input inviati dall'utente.
Consideriamo l'esempio che segue. Se abbiamo bisogno di validare i dati di un form, possiamo impostare un gestore di eventi. Al momento dell'invio dei dati, la funzione del gestore di eventi esegue un loop all'interno di una lista predeterminata di campi e determina come presentare gli errori che trova.
Questo codice dovrebbe esservi familiare:
function validateForm(){
var errorMessage = 'The following errors were found:<br>';
if (document.getElementById('email').value.length == 0) {
errorMessage += 'You must supply an email address<br>';
}
document.getElementById('message').innerHTML = errorMessage;
}
Questo approccio funziona, ma non è molto flessibile. Cosa succede se volessimo aggiungere dei campi o validare un form diverso su un'altra pagina? Dovremmo duplicare la maggior parte di questa funzionalità per ogni nuovo campo che aggiungiamo.
Verso la modularità
Il primo passo verso la modularità e la separazione consiste nell'incorporare elementi semantici aggiuntivi nel nostro form. Il campo in questione è un campo obbligatorio? Dovrebbe contenere un indirizzo e-mail? Se è così, potremmo usare qualcosa del genere:
<input type="text" class="required email">
Il nostro Javascript eseguirà un loop tra tutti i campi del form, estrapolerà i valori degli attributi definiti per la classe e agirà su di essi a partire da qui (l'attributo class
svolge in questo caso una duplice funzione, perché può essere usato anche come un aggancio per i CSS. Molto conveniente!).
Javascript legge i metadati (l'informazione che descrive i dati) e opera sui dati in base a quelle regole. Ma a questo punto, i dati e i metadati sono ancora troppo strettamente intrecciati con la struttura e la semantica del markup. Inoltre, questo metodo ha dei limiti. È difficile incorporare una logica condizionale nei costrutti dell'HTML. Per esempio, non possiamo dire che un campo è obbligatorio solo se un altro campo è stato compilato (o meglio, possiamo, ma ne risulta qualcosa di veramente orribile).
<input type="checkbox" name="other"> Other
<textarea class="dependson-other"></textarea>
Nell'esempio precedente, il prefisso dependson
indica che la textarea dipende dalla presenza di un altro campo, in questo caso di quello indicato con other
. Per evitare questo terribile approccio, proviamo a sviluppare questo tipo di logica in Javascript.
Usare Javascript per descrivere cose
Se possiamo incorporare un po' di semantica e dei metadati in HTML, abbiamo bisogno di avere quell'informazione all'interno del nostro layer Javascript. Descrivere dati in Javascript può essere estremamente comodo. Ecco un esempio:
var fields = {
'other': {
required:true
},
'additional: {
'required': {
'other':{checked:true},
'total':{between:[1,5]}
},
'only-show-if': {
'other': {checked:true}
}
}
};
In questo esempio, il campo additional
ha diverse dipendenze. Ognuna di queste dipendenze può essere descritta e può avere diversi livelli di informazione definiti al suo interno. In questo caso, il campo additional
richiede che due campi corrispondano a certe condizioni. E il campo additional
dovrebbe essere mostrato solo se l'utente ha spuntato l'altro checkbox.
A questo punto, Javascript viene usato per definire i campi e la logica di funzionamento che gestisce il modo in cui dovrebbe avvenire la validazione. Ad un livello abbiamo isolato parte del modello dei dati nel suo proprio oggetto, ma la validazione aspetta ancora che i dati siano disponibili in una variabile specifica. Aspetta anche un campo sulla pagina che mostri una vista sintetica di errori.
Sebbene si sia così ottenuto un pizzico di separazione, il processo di validazione ha ancora troppe dipendenze. La validazione dei dati e la presentazione degli errori di validazione sono ancora troppo legate. La funzione di validazione assicura comunque che l'evento venga catturato e che il form non venga inviato fino a quando tutto non sia corretto.
Nella seconda parte dell'articolo entreremo nei dettagli considerando come potrebbe presentarsi la struttura del nostro codice usando il pattern MVC sul nostro esempio di validazione.
Model
Dal momento che il pattern MVC ha tre componenti, dovremmo provare a separare la nostra applicazione in almeno tre oggetti principali.
Separare il Model nel suo proprio oggetto sarà facile: come abbiamo visto nel precedente esempio sulla validazione, ciò avviene in modo quasi naturale.
Diamo un'occhiata ad un altro esempio. Se avessimo un calendario di eventi, la data dell'evento sarebbe conservata nel suo oggetto. Metodi aggiunti all'oggetto astraggono il processo di interagire direttamente con i dati. Questi metodi sono spesso definiti con l'acronimo CRUD, che sta per "create, remove, update, delete" (crea, rimuovi, aggiorna, cancella).
var Events = {
get: function (id) {
return this.data[id];
},
del: function (id) {
delete this.data[id];
AjaxRequest.send('/events/delete/' + id);
},
data:{
'112': { 'name': 'Party time!', 'date': '2009-10-31' },
'113': { 'name': 'Pressies!', 'date': '2009-12-25' }
}
metadata: {
'name': { 'type':'text', 'maxlength':20 },
'date': { 'type':'date', 'between':['2008-01-01','2009-01-01'] }
}
}
Abbiamo anche bisogno di un modo per descrivere i dati, così aggiungeremo una proprietà che descrive i campi che un item può avere, compresi alcuni vincoli.
I compiti di tipo CRUD salvano i cambiamenti di stato sul server. In questo esempio, la funzione "delete" rimuove la voce dai suoi dati conservati localmente e quindi invia una richiesta al server per istruirlo a cancellare quello specifico item.
Quando si conservano dei dati, usate una chiave: è il modo più efficiente per estrarre i dati dal suo oggetto. Normalmente si tratta della chiave primaria definita nel database (l'ultimo esempio usa un id numerico). Per un calendario di eventi, conservare i valori separatamente per ciascun mese potrebbe essere più pratico. Ci evita di dover eseguire un loop tra tutti gli eventi provando a trovare quelli che devono essere inseriti nella pagina. Ovviamente, ciascuno dovrà trovare l'approccio che meglio funziona per il proprio progetto.
View
Nel pattern MVC, la View riceve i dati e stabilisce come visualizzarli. La View può usare il codice HTML esistente, può richiedere un nuovo blocco HTML dal server, può costruire nuovo codice HTML usando il DOM. Poi combina i dati forniti con la View e li mostra all'utente. È importante notare che alla View non importa come ottenere i dati o da dove i dati stessi provengono.
View.EventsDialog = function(CalendarEvent) {
var html = '<div><h2>{name}</h2>' +
'<div class="date">{date}</div></div>';
html = html.replace(/{[^}]*}/g, function(key){
return CalendarEvent[key.slice(1,-1)] || '';
});
var el = document.getElementById('eventshell');
el.innerHTML = html;
}
var Events.data = {
'112': { 'name': 'Party time!', 'date': '2009-10-31' },
'113': { 'name': 'Pressies!', 'date': '2009-12-25' }
}
View.EventsDialog(Events.data['112']); // edits item 112
Se osserviamo l'esempio precedente, notiamo che abbiamo tre parti: la funzione EventsDialog che aspetta un oggetto JSON con le proprietà relative al nome e alla data; la proprietà Events che conserva gli eventi del calendario; la chiamata che manda uno specifico evento a EventsDialog.
La View per Events Dialog può essere estesa per includere metodi addizionali che rendono possibile l'interazione. Nell'esempio che segue, a Events Dialog vengono assegnati metodi per l'apertura e la chiusura. Così facendo facciamo sì che la View fornisca dei punti di aggancio che consentano al Controller di gestire la View senza aver bisogno di conoscere i dettagli interni dell'oggetto.
View.EventsDialog = function(CalendarEvent){ ... }
View.EventsDialog.prototype.open = function(){
document.getElementById('eventshell').style.display = 'block';
}
View.EventsDialog.prototype.close = function(){
document.getElementById('eventshell').style.display = 'none';
}
var dialog = new View.EventsDialog(eventObject);
dialog.open();
dialog.close();
Generalizzare le View
Rendendo le View consapevoli del modello di dati e del metodo per l'estrazione dei dati rischiamo di cadere in una trappola. Separando queste funzioni, tuttavia, possiamo riusare l'interfaccia di dialogo per altre cose. In questo esempio, se separiamo i dati dell'evento e l'interfaccia di dialogo, possiamo generalizzare quest'ultima per visualizzare e modificare qualunque modello di dati, non solo gli eventi.
View.Dialog = function(data) {
var html = '<h2>' + data.name + '</h2>';
delete data.name;
for(var key in data) {
html += '<div>' + data[key] + '</div>';
}
var el = document.getElementById('eventshell');
el.innerHTML = html;
}
Ora abbiamo un modo generico per visualizzare gli item per qualsiasi oggetto, non solo gli eventi. Nel prossimo progetto in cui avremo bisogno di un'interfaccia di dialogo, basterà inserire questo pezzo di codice.
Molti framework Javascript sono progettati per con questa sorta di agnosticismo rispetto ai dati. I controlli della libreria YUI, i widget di jQuery UI, ExtJS e Dojo Dijit sono costruiti tenendo conto di questa generalizzazione. Il risultato è che è facile incrementare le applicazioni con controlli esistenti.
Gestire i metodi della View
Come regola generale, una View non dovrebbe eseguire i suoi metodi. Per esempio, un box di dialogo non dovrebbe aprire o chiudere se stesso. Lasciamo questi compiti al Controller.
Se un utente clicca su un pulsante "Salva" all'interno di un box di dialogo, quell'evento viene viene passato ad un'azione del Controller. L'azione può allora decidere cosa dovrebbe fare la View. Forse chiude il box di dialogo. Oppure dice alla View di mostrare una barra di avanzamento mentre i dati sono salvati. Una volta che i dati sono stati salvati, l'evento di completamento Ajax fa scattare un'altra azione del controller che dice alla View di nascondere l'indicatore e chiudere il box di dialogo.
Tuttavia, ci sono situazioni in cui le View dovrebbero gestire i loro eventi o eseguire i loro metodi. Per esempio, una View potrebbe avere uno slider per selezionare dei valori. La View gestisce la logica per l'interazione dello slider e il modo in cui essa mostra i risultati della selezione. Non c'è bisogno di un Controller che gestisca quella interazione.
Il Controller
Ora, come facciamo a portare i dati del Model alla View? Ecco a cosa serve il Controller. Un Controller si attiva dopo che si verifica un evento. Può avvenire quando si carica la pagina o quando l'utente compie un'azione. Un gestore di eventi viene assegnato ad un metodo del Controller che farà in modo che venga eseguita l'azione che l'utente vuole compiere.
Controllers.EventsEdit = function(event) {
/* event is the javascript event, not our calendar event */
// grab the event target id, which stores the id
var id = event.target.id.replace(/[^d]/g, '');
var dialog = new View.Dialog( Events.get(id) );
dialog.open();
}
Questo pattern è davvero comodo quando i dati sono usati in vari contesti. Per esempio, diciamo che stiamo modificando un evento mostrato sul calendario. Clicchiamo sul pulsante "Cancella" e ora abbiamo bisogno di far scomparire il box di dialogo e l'evento sul calendario, e poi di cancellare l'evento dal server.
Controller.EventsDelete = function(event) {
var id = event.target.id.replace(/[^d]/g, '');
View.Calendar.remove(id);
Events.del(id);
dialog.close();
}
Le azioni del controller diventano più semplici e facili da comprendere. Questa è la chiave per costruire un'applicazione facile da mantenere.
Validare il nostro Model
Il Model determina se i dati sono corretti o non usano un metodo. Non si occupa del modo in cui presentiamo la vista riassuntiva. Ha solo bisogno di riportare quali campi non funzionano.
In precedenza, nell'esempio sulla validazione, avevamo una semplice variabile chiamata fields
che conteneva dei metadati sul nostro modello di dati. Possiamo estendere l'oggetto con un metodo che può comprendere e ispezionare i dati passati ad esso. Nell'esempio che segue, abbiamo aggiunto un metodo validate
all'oggetto. Puo eseguire un loop tra i dati e confrontare i dati rispetto ai requisiti definiti all'interno dei suoi metadati.
var MyModel = {
validate: function(data) {
var invalidFields = [];
for (var i = 0; i < data.length; i++) {
if (this.metadata[data.key].required && !data.value) {
invalidFields[invalidFields.length] = {
field: data.key,
message: data.key + ' is required.'
};
}
}
return invalidFields;
},
metadata: {
'other': {required:true}
}
}
Per validare i nostri dati forniamo un array di coppie chiave/valore. La chiave è il nome del campo e il valore è quello che l'utente ha inserito nel campo.
var data = [
{'other':false}
];
var invalid = MyModel.validate(data);
La nostra variabile invalid
ora contiene una lista dei campi che non sono validi. Ora passeremo i datti alla View per mostrare gli errori sulla pagina.
Presentare i campi non validi
In questo caso, abbiamo bisogno di mostrare un messaggio di errore sulla pagina. Questa visualizzazione sarà una View e si aspetta che i dati siano passati ad essa dal Controller. La View userà quei dati per costruire un messaggio di errore che viene mostrato all'utente. Dal momento che l'abbiamo scritta in maniera generalizzata, questa View può essere usata in varie circostanze.
View.Message = function(messageData, type){
var el = document.getElementById('message');
el.className = type;
var message = '<h2>We have something to bring to your »
attention</h2>' +
'<ul>';
for (var i=0; i < messageData.length; i++) {
message += '<li>' + messageData[i] + '</li>';
}
message += '</ul>';
el.innerHTML = message;
}
View.Message.prototype.show() {
/* provide a slide-in animation */
}
Type
è attaccato all'elemento DOM come classe CSS, consentendoci di applicare stili al messaggio di errore. Poi, una funzione esegue un loop tra i dati del messaggio e lo inserisce nella pagina.
Agganciare tutto con un Controller
Il nostro Model conserva i dati e può dirci che i dati sono validi. Abbiamo una View, il messaggio di errore, che usiamo per visualizzare il feedback per un'operazione riuscita o fallita all'utente. L'ultimo passo è quello di validare il form quando l'utente prova a inviarlo.
/* use the event binding of your friendly neighbourhood
JavaScript library or whatever you like to use
*/
addEvent(document.getElementById('myform'), 'submit', »
MyController.validateForm);
Con il nostro evento collegato all'azione del Controller, catturiamo i dati, li validiamo e presentiamo gli eventuali errori.
MyController.validateForm = function(event){
var data = [];
data['other'] = document.getElementById('other').checked;
var invalidFields = MyModel.validate(data);
if (invalid.length) {
event.preventDefault();
// generate the view and show the message
var message = new View.Message(invalidFields, 'error');
message.show();
}
}
L'array dei dati contiene i valori immessi nei campi. Il Model valida i dati e restituisce una lista di campi non validi. Se ci sono campi non validi l'invio del form viene bloccato e i dati di Message
sono passati alla View. Dopodiché la View mostra l'errore sulla pagina.
Fatto! Ci restano una View con un messaggio riutilizzabile e un Model con dei metodi di validazione.
Gestire il Progressive Enhancement
Nell'esempio della validazione, la struttura MVC funziona bene con il progressive enhancement. Separando il nostro codice, saranno di meno i componenti che avranno bisogno di capire cosa sta accadendo sulla pagina. Questa separazione può di fatto rendere più semplice il progressive enhancement. Ci sono infatti meno riferimenti a Javascript nel codice HTML.
Con una struttura MVC come questa, potremmo voler caricare la pagina e fare poi una richiesta Ajax per estrarre i dati iniziali dal server e popolare con dei dati la vista iniziale. Tuttavia, ciò può portare all'impressione di un'interfaccia più lenta, dal momento che devono essere fatte due richieste prima che l'utente possa interagire con la pagina: la prima richiesta per caricare la pagina, la seconda per caricare i dati nella pagina.
Invece, carichiamo la pagina normalmente, con dati statici. Questo è lo stato iniziale. I dati contenuti nello stato iniziale sono anche conservati come Javascript in fondo alla pagina. Una volta che la pagina viene caricata, il layer Javascript della nostra applicazione è pronto per l'uso.
Possono anche essere precaricati dati addizionali. Nell'esempio del calendario, gli eventi per il mese corrente sono presenti direttamente nel codice HTML, ma il layer Javascript può essere reso consapevole anche dei dati per il mese precedente e per quello successivo. Il tempo di caricamento in più che è richiesto è minimo ma l'utente può spostarsi avanti e indietro senza dover richiedere nuovi dati dal server.
Framework
I faramework Javascript MVC che stanno nascendo forniscono un approccio più strutturato e potente allo sviluppo MVC di quello che ho espresso in questo articolo. Capire bene come questo pattern può essere applicato al vostro lavoro dovrebbe comunque aiutare, sia che usiate un framework da voi creato sia che usiate un framework esistente.
Alcuni framework MVC sono:
Se avete o no bisogno di un framework formale dipende dalla complessità della vostra appliazione. Se è semplice, probabilmente non ne vale la pena.
Conclusione
Come qualsiasi altra cosa nello sviluppo, dovrete decidere se i vantaggi derivanti da questo tipo di separazioni sono tali da garantire una maggiore efficienza ed efficacia. Per piccole applicazioni dove si hanno solo poche funzioni, questo tipo di separazione è eccessiva. Più l'applicazione è grande, tuttavia, più otterrete dei benefici separando il codice in Model, View e Controller.