Nella nostra serie dedicata alla prototipazione attraverso MEAN.io, intendiamo realizzare un’applicazione che utilizza JavaScript come linguaggio di sviluppo, sia lato frontend che lato backend. In questa quarta parte, dedichiamo le nostre attenzioni agli aspetti tecnici relativi alla parte server, utilizzando Node.js e il database MongoDB.
Il lato backend del nostro progetto
Dopo aver parlato della progettazione della UX nelle parti precedenti, con questo articolo cominciamo a mettere il naso nella parte “server” nel nostro progetto MEAN.io. Trattandosi di un progetto Node.js, il codebase dell’applicazione, sia frontend che backend, utilizza JavaScript come linguaggio di sviluppo.
Questo generalmente crea un po’ di confusione per chi non è abituato a trattare progetti Node.js: spesso si tende a scambiare i ragionamenti che si fanno fra frontend e backend, anche in virtù del fatto che è possibile utilizzare le medesime librerie JavaScript in entrambe le situazioni.
Per i nostri fini di rapid prototyping non è necessario addentrarci nei meandri delle best practice Node.js: ci basta al momento sapere che oggi familiarizzeremo con la parte “server” del progetto che risiede in una directory “app” ben separata da quella client.
Figura 1 – La directory “app” del nostro progetto.
La directory app
Nella directory “app” troveremo tutto quello che riguarda il nostro backend, convenzionalmente suddiviso in queste sottocartelle:
- controllers: qui risiedono tutte le classi dei controller invocate dalle routes e contententi la business logic dell’applicazione;
- models: contenenti le classi di modello dell’applicazione;
- routes: la directory delle routes o delle “rotte”, ossia gli URL, invocati con GET / POST, a cui l’applicazione risponde invocando il rispettivo metodo nei controller e visualizzando il risultato sottoforma di HTML;
- views: eventuali viste o frammenti HTML che devono essere visualizzati ad hoc.
La directory config/env
Nella directory “config/env” sono posizionati i file di configurazione con i settaggi per gli ambienti di produzione, sviluppo e test, in particolare l’URL del database e le API Key per i login attraverso Facebook, Twitter (OAuth) etc. nel caso volessimo usufruire di questo tipo di autenticazioni, in gergo definite Authentication Strategies.
Se non avete impostato porte di connessione diverse da quelle predefinite per particolari esigenze vostre, per ora possiamo lasciare tutto a default.
“node_modules” contiene tutti i moduli su cui la nostra applicazione dipende, moduli dichiarati nel grunt file che abbiamo già visionato insieme nella prima parte.
Le altre directory
Ignoriamo la cartella “test“, in cui è presente uno scheletro di struttura di test unitari per la nostra applicazione. Sarebbe buona norma partire sempre scrivendo i test prima ancora della business logic, ma non tratteremo questa parte in questa serie.
Tutto quel che riguarda il frontend lo troveremo invece sotto la cartella “public“, dove sono servite le risorse statiche dell’applicazione, ovvero i canonici PNG, CSS, JS e HTML a cui siamo abituati.
Vi ricordate cosa rappresenta la “A” nell’acronimo di MEAN? AngularJS [1]. Essendo Angular un framework JS di frontend, lo troveremo appunto nella directory public quando affronteremo la parte client dell’applicazione.
Figura 2 – Uno schema dell’architettura della nostra applicazione (immagine da scotch.io).
Dove ci eravamo lasciati? La lista della spesa
Vediamo adesso cosa ci offre lo scaffolding standard di MEAN.io. Posizioniamoci nella root del progetto e avviamo attraverso una finestra di shell, prima di tutto MongoDB:
Figura 3 – Avviamo MongoDB da una finestra di shell.
MongoDB sarà accessibile ora su localhost alla porta di default 27017 e, nella shell corrente, avremo tutti i log degli errori che potremo fare. Avviamo quindi il progetto in un’altra shell, semplicemente con “grunt”. Grunt leggerà il gruntfile, scaricherà nella directory “node_modules” eventuali dipendenze che non avevamo scaricato la volta precedente e avvierà il server di Node.js che sarà accessibile su localhost alla porta di default 3000.
Figura 4 – Avviamo il server di Node.js
Apriamo il browser e visitiamo quindi “localhost:3000“.
Figura 5 – Ed ecco la pagina che ci appare.
La prima cosa che vogliamo fare è cliccare sulla action “Signup” in alto a destra e creare un account:
Figura 6 – Strategie di autenticazione
Potete tranquillamente usare dei dati fittizi: non è prevista una doppia validazione. Out-of-the-box, MEAN.io supporta 5 strategie di autenticazione, oltre alla canonica Username e Password. Creiamo quindi un nuovo account.
Figura 7 – Ad account creato, siamo reindirizzati sulla home, ma questa volta siamo loggati.
Appena creato l’account, saremo reindirizzati alla HOME già loggati. Come utente loggato si sono abilitati due ulteriori azioni: “Articles” e “Create New Article“.
Figura 8 – Ora siamo autenticati e quindi abbiamo a disposizione nuove azioni.
“Article” non è altro che la visualizzazione della lista degli “Articles” presenti nel sistema, mentre l’altra action è la form di input con la quale possiamo creare un nuovo “Article“.
Figura 9 – La form per creare un nuovo articolo.
Creiamo un nuovo articolo e inviamolo.
Figura 10 – Ecco il nuovo articolo appena creato, con titolo e testo.
Di fatto il progetto di base di MEAN.io implementa due entità: USER per la gestione degli utenti e ARTICLE, che è semplicemente un CRUD ed è esattamente quello che ci serve per prototipare la Einkaufsliste. La lista della spesa, in maniera molto semplicistica, può essere infatti ricondotta a due CRUD principali: la LISTA e l’elemento ITEM di lista.
Dietro le quinte
Per comprendere meglio di cosa stiamo parlando e quello che abbiamo fatto, diamo un’occhiata a cosa è cambiato all’interno del database. Esistono una quantità di strumenti visuali e non in grado di spulciare dentro MongoDB: sul sito ufficiale [2] ne trovate davvero un’ampia selezione, sia gratuiti che a pagamento che in opensource.
La mia personale preferenza va a Robomongo [3], un client visuale che ha un’impostazione deliziosamente shell-based: tutto quello che è possibile fare via shell, è possibile farlo via Robomongo e viceversa; e ogni comando viene presentato sempre con il corrispondente comando testuale.
Dopo l’installazione dipendente dal vostro sistema operativo, sarà sufficiente lanciare l’applicativo e creare una connessione a localhost sulla porta di default di MongoDB e connettersi.
Figura 11 – Con Robomongo possiamo connetterci a MongoDB.
MongoDB è un database NoSQL, non relazionale, che immagazzina i dati in documenti che incidentalmente hanno la struttura di oggetti JavaScript. Se già maneggiate oggetti JSON, non avrete alcuna difficoltà a interfacciarvi con questa tipologia di database.
Il DB che MEAN.io utilizza a default è “mean-dev“, nel quale sono presenti 3 collezioni, create nell’istante in cui abbiamo fatto girare l’applicativo demo di MEAN.io: Users, Articles e Sessions:
- users è la collezione che raccoglie i nostri utenti;
- sessions è la tabella di appoggio utilizzata da Passport per gestire le sessioni utente;
- articles: è la tabella che contiene i CRUD degli articoli.
Figura 12 – La collezione “users” del DB di default “mean-dev”.
Come ci aspettavamo, in users troveremo un’unica voce, corrispondente all’account che abbiamo creato: nome, email e username come chiave unica. La password è salvata come hashed criptato e il provider indica come l’account è stato creato.
Notate come In Robomongo viene anche visualizzato il comando necessario per visualizzare tutte le entry di una collezione, in questo caso: db.users.find();
Figura 13 – La collezione “articles” del DB di default “mean-dev”.
La collezione articles ha invece, come ci potevamo aspettare, due documenti: il primo articolo già presente a default e il secondo articolo che abbiamo inserito.
Al lavoro
Il punto di ingresso per ogni applicazione Node.js è “server.js“. Non entreremo nel dettaglio di com’è strutturato, perchè in questo articolo non lo toccheremo: vanno comunque notati i moduli che sono stati inseriti come “require“:
/* ---------------------------- server.js ---- */ 'use strict'; /** * Module dependencies. */ var express = require('express'), fs = require('fs'), passport = require('passport'), logger = require('mean-logger'); ... var config = require('./config/config'), mongoose = require('mongoose'); ... /* ---------------------------- server.js --*/
Ci sono tre moduli sui quali vale la pena di spendere due parole, in particolare Express (la lettera E dell’acronimo di MEAN), Passport e Mongoose.
Express
Express [4] è un framework essenziale e minimalista nato per facilitare la creazione di infrastrutture base per web application e API su piattaforma Node.js.
Essenzialmente fornisce una struttura per modellare oggetti e fornire il supporto per le rotte (routes). Ha un carattere spiccatamente modulare, integrandosi perfettamente con tutta una serie di moduli e middleware specializzati: ad esempio è agnostico nei confronti del database, il che ci permette di interfacciarlo, come nello stack MEAN, con MongoDB.
Ci troviamo nel mondo di Node.js, la cui filosofia è quella di fornire solo pochi componenti prefabbricati omnicomprensivi e monolitici One-Size-Fits-All, preferendo invece piccoli moduli, mattonicini di base che possono essere presi e combinati fra loro a seconda delle esigenze puntuali.
Passport
Come avete visto dalla schermata di “signup” di MEAN.io, il framework ci fornisce un’ottima base di partenza per l’autenticazione e la gestione basica degli utenti, attraverso Passport [5]. Passport è un middleware di autenticazione per Node.js, estremamente semplice da inserire nei progetti e allo stesso tempo completo.
Si integra perfettamente con Express.js e fornisce diverse strategie di autenticazione, dal canonico username e password che abbiamo avuto modo di sperimentare, alle strategie di autenticazione attraverso OpenID e OAuth: in particolare ne faremo uso prossimamente per autenticarci via Facebook e Twitter.
Mongoose
Moongoose [6] è il middleware che ci permette di interfacciarci con MongoDB senza essere obbligati a reinventare la ruota e a scrivere a mano un wrapper per le nostre query NoSQL.
Mongoose ci permette di modellare i nostri oggetti Node.js utilizzando delle entità basate su schema dichiarati che incapsulano tutta la logica di casting, validazione e costruzione di query necessaria per interfacciarsi al DB.
L’eleganza di Mongoose risiede nel fatto che, una volta dichiarato da quali campi è composto un singolo oggetto, continueremo a utilizzare le nostre classi JavaScript in maniera del tutto naturale e la persistenza viene gestita in modo trasparente invocando un metodo .save() sull’oggetto stesso.
In lettura viene utilizzato il medesimo approccio, invocando un metodo .find(object, callback) al quale passeremo gli appositi parametri di ricerca incapsulati in un object JavaScript e una callback al quale vogliamo delegare la gestione del risultato. La gestione manuale di MongoDB ci viene fortunatamente schermata.
Lavoriamo sul nostro progetto
Per quel che riguarda il nostro progetto non abbiamo nessun problema a riutilizzare la classe user così com’è, mentre. per il nostro oggetto lista della spesa possiamo indubbiamente riutilizzare come base article e ricalcarne la struttura.
Creiamo un file itemList.js nella directory app/models. Quello che ci apprestiamo a fare è creare un modello Mongoose per la lista, e lo facciamo dichiarando un ItemListaSchema che rappresenta lo schema dell’entità con tutti i campi che la caratterizzano; creiamo il modello e infine leghiamo il modello con lo schema.
Ereditando da Mongoose, potremo maneggiare l’entità ItemList attraverso metodi wrapper come save() e find().
/* ---------------------------- app/models/itemList.js -- */ 'use strict'; /** * Dipendenze mongoose. */ var mongoose = require('mongoose'), Schema = mongoose.Schema; /** * Dichiarazione Schema ItemList */ var ItemListSchema = new Schema({ user: { type: Schema.ObjectId, ref: 'User' }, dest: { type: Schema.ObjectId, ref: 'User' }, title: { type: String, /* TODO Stringhe da parametrizzare */ default: 'Lista', trim: true }, items: [{ type : Schema.ObjectId, ref: 'Item' }], created: { type: Date, default: Date.now }, sent: { type: Date, default: Date.now }, status: { type: Number, default: 0 } }); /** * Validazione, il titolo non deve essere vuoto */ /* TODO Stringhe da parametrizzare */ ItemListSchema.path('title').validate(function(title) { return title.length; }, 'Ogni lista deve avere un nome'); /** * Statics */ ItemListSchema.statics.load = function(id, cb) { this.findOne({ _id: id }).populate('user', 'name username') .populate('dest', 'name username') .populate('items').exec(cb); }; /** * Creazione del modello e associazione al relativo schema */ mongoose.model('ItemList', ItemListSchema); /* ---------------------------- app/models/itemList.js -- */
Ogni entità itemList avrà quindi un riferimento a un’entità user che ne rappresenta il creatore, un secondo riferimento a un’entità user che ne rappresenta il destinatario, un titolo, una data di creazione, una data di invio, un intero di status che verrà utilizzato per determinare lo stato della lista (0 creata | 1 inviata | 2 visionata | 3 chiusa) e una collezione di entità item, ossia gli elementi della lista.
È definita una validazione per forzare il fatto che titolo non possa essere vuoto ed è stato inserito un metodo statico load che si occupa di popolare il riferimento a user nel momento in cui è letta l’entità.
Una volta creato il modello, dobbiamo creare il file itemList.js nella directory delle rotte, app/routes che otteniamo replicando quello già presente per articles.js e modificandolo opportunamente.
/* ---------------------------- app/routes/itemList.js -- */ 'use strict'; /* Nella rotta facciamo uso del controller omonimo * e del modulo di autorizzazione */ var itemLists = require('../controllers/itemLists'); var authorization = require('./middlewares/authorization'); /* Permessi sulla visualizzazione */ var hasAuthorization = function(req, res, next) { if (req.itemList.user.id !== req.user.id || req.itemList.dest.id !== req.user.id) { /* TODO Stringhe da parametrizzare */ return res.send(401, 'Non puoi modificare liste di cui non fai parte'); } next(); }; /* Rotte */ module.exports = function(app) { app.get('/itemLists', itemLists.all); app.post('/itemLists', authorization.requiresLogin, itemLists.create); app.get('/itemLists/:itemListsId', itemLists.show); app.put('/itemLists/:itemListsId', authorization.requiresLogin, hasAuthorization, itemLists.update); app.del('/itemLists/:itemListsId', authorization.requiresLogin, hasAuthorization, itemLists.destroy); /* Setting del parametro passato */ app.param('itemListsId', itemLists.itemList); }; /* ---------------------------- app/routes/itemList.js -- */
Le rotte definite sono quelle che esportiamo nella forma:
app.<GET|POST|PUT|DELETE>(‘', , …, )
In questo caso abbiamo due rotte in GET, una che risponde all’URL /itemLists che invoca il metodo del controller items.all(), e l’altra all’URL /itemLists/ che invoca items.show(), rispettivamente per visualizzare tutte le liste e per visualizzarne una precisa a cui passiamo l’identificativo.
C’è un metodo in POST per la creazione della lista preceduto da un filtro di autenticazione e dai metodi rispettivamente per modificare ed eliminare una lista che hanno un ulteriore filtro booleano (hasAuthorization) che controlla che l’ID dell’utente sia effettivamente almeno un creatore o un destinatario della lista per poter invocare il metodo rispettivo del controller.
Non ci resta quindi che creare il file controller itemLists.js in app/controllers/: anche questo non presenta per ora variazioni rispetto a quello di articles.js.
Notate la convenzione nel nome che prevede il plurale quando andiamo a operare sui controllers.
/* ---------------------------- app/controllers/itemLists.js -- */ 'use strict'; /** * Dipendenze: mongoose e il modello di ItemList * Lodash come utility belt */ var mongoose = require('mongoose'), ItemList = mongoose.model('ItemList'), _ = require('lodash'); /** * Recuperare un itemList by id */ exports.itemList = function(req, res, next, id) { ItemList.load(id, function(err, itemList) { if (err) return next(err); if (!itemList) return next(new Error('Failed to load itemList ' + id)); req.itemList = itemList; next(); }); }; /** * Creare un itemList */ exports.create = function(req, res) { var itemList = new ItemList(req.body); itemList.user = req.user; /* In caso di errore l'utente non è loggato */ itemList.save(function(err) { if (err) { return res.send('users/signup', { errors: err.errors, itemList: itemList }); } else { /* SUCCESS */ res.jsonp(itemList); } }); }; /** * Update di itemList */ exports.update = function(req, res) { var itemList = req.itemList; itemList = _.extend(itemList, req.body); itemList.save(function(err) { if (err) { /* In caso di errore l'utente non è loggato */ return res.send('users/signup', { errors: err.errors, itemList: itemList }); } else { /* SUCCESS */ res.jsonp(itemList); } }); }; /** * Cancellazione di itemList */ exports.destroy = function(req, res) { var itemList = req.itemList; itemList.remove(function(err) { if (err) { /* In caso di errore l'utente non è loggato */ return res.send('users/signup', { errors: err.errors, itemList: itemList }); } else { /* SUCCESS */ res.jsonp(itemList); } }); }; /** * Visualizzazione di itemList */ exports.show = function(req, res) { res.jsonp(req.itemList); }; /** * Visualizzazione di tutte le ItemLists */ exports.all = function(req, res) { ItemList.find().sort('-created').populate('user', 'name username').exec(function(err, itemLists) { if (err) { /* Ooops qualcosa non va a buon fine */ res.render('error', { status: 500 }); } else { /* SUCCESS */ res.jsonp(itemLists); } }); }; /* ---------------------------- app/routes/itemLists.js -- */
Il controller deve prevedere attraverso l’espansione della proprietà exports, tutti i metodi che andiamo a definire nel file delle rotte, exports.. Avremo quindi un metodo che prende in ingresso un parametro id, un metodo create(), un update(), un destroy() e un all(), a seconda delle casistiche.
Cosa possono fare questi metodi nel controller? Generalmente questi metodi prendono in ingresso una request e una response, operando internamente sull’oggetto response con 3 modalità:
- res.render(), che ritorna direttamente un valore, ad esempio per popolare una vista utente;
- res.send(), un redirect a un’altra rotta, un esempio è la form di signup;
- res.jsonp(), ossia la restituzione di un risultato in formato JSON, comodo nel momento in cui si creino delle API o si lavori, come in questo scenario, con AngularJS.
Ricapitolando
Se non avete fermato la shell con cui avete avviato Node.js, avrete sicuramente notato che ogni volta che salvavate un file nelle directory supervisionate dal progetto, Grunt è rimasto in ascolto dei cambiamenti sui file e lo ha mandato prontamente in esecuzione, segnalandovi in tempo reale eventuali errori JavaScript.
Questo è estremamente utile per evitare errori di battitura o distrazione nei file .js, qualora non utilizzaste un IDE. In conclusione non ci resta che replicare quello che abbiamo fatto con la classe ItemList, facendolo con una nuova classe Item. Vi lascio quindi con la definizione della ItemSchema, con il compito di completare il resto.
/** * Dichiarazione Schema Item */ var ItemSchema = new Schema({ user: { type: Schema.ObjectId, ref: 'User' }, title: { type: String, /* TODO Stringhe da parametrizzare */ default: 'Item', trim: true }, created: { type: Date, default: Date.now }, unit: { type: String, /* TODO Stringhe da parametrizzare */ default: 'pcs', trim: true }, position: { type: Number, default: 0 }, status: { type: Number, default: 0 }, parents: [{ type : Schema.ObjectId, ref: 'ItemList' }] });
Conclusioni
In questo articolo abbiamo esposto parecchi temi, concentrandoci sul backend del nostro progetto. La prossima volta ci occuperemo della parte frontend e cominceremo a incastrare tutti i singoli pezzi.