Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Un 'dispatcher' per gestire il traffico

Realizzare un semplice modulo "controller" per dirigere il traffico in arrivo all'applicazione
Realizzare un semplice modulo "controller" per dirigere il traffico in arrivo all'applicazione
Link copiato negli appunti

Dopo aver introdotto in maniera teorica (e parziale) il vasto mondo di NodeJS, entriamo sul pratico e iniziamo a confrontarci con le classiche tematiche che ruotano intorno al concetto di applicazione Web. In questo articolo e nei seguenti affronteremo queste problematiche attraverso la realizzazione di un'applicazione realmente funzionante.

Per sviluppare l'applicazione ci avvarremo di alcune librerie presenti in NPM in quanto quelle native incluse in NodeJS non offrono un bacino così ampio di funzionalità ma offrono solamente gli strumenti di base. NPM è il principale repository di librerie di terze parti di NodeJS ed è stato approfondito negli articoli precedenti.

Il concetto di dispatcher

Riprendiamo per un attimo la primissima applicazione scritta in questa guida:

var http = require('http');
var server = http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Hello Worldn');
})
server.listen(1337, '127.0.0.1');
console.log('Server running at http://127.0.0.1:1337/');

Questo codice esegue perfettamente ciò per cui è stato scritto: rispondere con "Hello World" a qualsiasi chiamata effettuata sulla porta 1337.

Le applicazioni web però non sono così banali e, solitamente, espongono una serie di "punti di accesso" per le diverse funzionalità offerte. Questi punti di accesso vengono tecnicamente tradotti in gergo tecnico nel concetto di URL. A seconda dell'URL invocato dell'utente, manualmente, tramite un link, un form o una chiamata AJAX, l'applicazione si comporterà diversamente.

Il primo componente che analizzeremo sarà appunto un dispatcher che permetterà di differenziare le chiamate effettuate dall'utente. Questo componente, data la sua fondamentale importanza in qualsiasi progetto, verrà sviluppato sotto forma di modulo permettendone quindi un rapido riutizzo in più applicazioni.

Il componente in questione è disponibile sia su NPM che su GitHub ed è stato scritto dal sottoscritto proprio allo scopo di essere utilizzato nel concreto. Il componente offre anche una funzionalità che non verrà approfondita in questa guida, i filtri, ma che sono facilmente comprensibili una volta analizzato il modulo.

Creare il modulo

Come abbiamo visto in precedenza un modulo non è nient'altro che un file contenente del codice JavaScript che espone le sue funzionalità attraverso l'oggetto nativo module.exports. In questo file creeremo la classe HttpDispatcher e ne esporremo una sua istanza, dato che l'oggetto segue il pattern singleton. Lo sviluppo seguirà un flusso incrementale, quindi aggiungeremo passo passo le varie funzionalità.

Prima di analizzare il codice del modulo soffermiamoci su quello che sarà il risultato finale che nostra applicazione dovrà raggiungere dopo averlo implementato:

var dispatcher = require('httpdispatcher'); 
dispatcher.onGet("/page1", function(req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Page One');
}); 
dispatcher.onPost("/page2", function(req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Page Two');
}); 
http.createServer(function (req, res) {
  dispatcher.dispatch(req, res);
}).listen(1337, '127.0.0.1');

Il codice è piuttosto intuitivo da leggere: abbiamo creato due listeners in ascolto rispettivamente all'url /page1 invocato dal metodo GET e all'url /page2 invocato dal metodo POST. Entrambi i listeners risponderanno con un testo semplice in quanto non è argomento di questa lezione concentrarsi sul contenuto ma bensì sull'instradamento delle varie richieste. Una volta compreso quale deve essere il nostro risultato passiamo alla scrittura del modulo vero e proprio.

var HttpDispatcher = function() {
  this.listeners = { get: [ ], post: [ ] };
} 
HttpDispatcher.prototype.on = function(method, url, cb) {
  this.listeners[method].push({
    cb: cb,
    url: url
  });
}
HttpDispatcher.prototype.onGet = function(url, cb) {
  this.on('get', url, cb);
}
HttpDispatcher.prototype.onPost = function(url, cb) {
  this.on('post', url, cb);
}

Il dispatcher (che ricordiamoci sarà sempre uno per applicazione) avrà tra le sue proprietà una lista di listeners organizzati per metodo HTTP (in questo caso solamente i metodi più comuni GET e POST) che rappresenteranno le diverse callback definite dall'utente. Oltre a queste proprietà, che risulteranno di fatto private, definiamo le funzioni per andare a definire nuovi listeners.

Ona volta popolata la proprietà listeners dobbiamo occuparci di differenziare effettivamente la chiamata all'interno del metodo dispatch (che viene invocato direttamente dall'applicazione).

HttpDispatcher.prototype.dispatch = function(req, res) {
  var parsedUrl = require('url').parse(req.url, true);
  var method = req.method.toLowerCase();
  this.listener[method][parsedUrl.pathname](req, res);
}
module.exports = new HttpDispatcher();

Il metodo in questione è banale. Viene parsato l'url tramite il modulo url e il metodo parse e, una volta identificato il metodo e il path viene invocato il listener appropriato passando come parametri gli oggetti request e response.

Infine viene esposta un'istanza della classe HttpDispatcher.

La gestione degli errori

Il dispatcher per come è fatto ora funziona senza problemi, ma non gestisce le richieste di URL diversi da quelli previsti. Per risolvere anche queste eventualità possiamo aggiungere un dispatcher particolare che viene invocato ogni volta che un utente invoca un URL non gestito.

var HttpDispatcher = function() {
  this.listener = { get: { }, post: { } };
  this.errorListener = function() { }
}
HttpDispatcher.prototype.onError = function(cb) {
  this.errorListener = cb;
}
HttpDispatcher.prototype.dispatch = function(req, res) {
  var parsedUrl = require('url').parse(req.url, true);
  var method = req.method.toLowerCase();
  if(this.listener[method][parsedUrl.pathname]) this.listener[method][parsedUrl.pathname](req, res)
  else this.errorListener(req, res);
}

È stato aggiunta, nel costruttore, una nuova proprietà privata errorListener, un metodo setter per essa e il controllo all'interno del metodo dispatcher che controlla preventivamente se l'URL invocato presenta un listeners associato e in caso contrario invoca l'errorListener.

Per poter utilizzare questa nuova funzionalità basta richiamare all'interno dell'applicazione le risorse statiche.

Le risorse statiche

In una applicazione web che si rispetti esistono due tipi di risorse:

  • dinamiche, che richiedono un'elaborazione del server e che inviano di solito una response contenente HTML
  • statiche, che fanno riferimento ad oggetti presenti sul file system, come fogli di stile CSS, script JavaScript (in questo caso client-side) o immagini

L'ultima funzionalità che inseriremo nel dispatcher sarà quella che permette di definire una cartella come statica, perché qualsiasi URL che punti a tale cartella ottenga in risposta il file fornito senza nessuna elaborazione.

var HttpDispatcher = function() {
  this.listeners = { get: [ ], post: [ ] };
  this.errorListener = function() { }
  this.staticFolderPrefix = '/static';
}
HttpDispatcher.prototype.setStatic = function(folder) {
  this.staticFolderPrefix = folder
}
HttpDispatcher.prototype.staticListener = function(req, res) {
  var url = require('url').parse(req.url, true);
  var filename = require('path').join(".", url.pathname);
  var errorListener = this.errorListener;
  require('fs').readFile(filename, function(err, file) {
    if(err) {
      errorListener(req, res);
      return;
	}
    res.writeHeader(200,
	                { "Content-Type": require('mime').lookup(filename) });
    res.write(file, 'binary');
    res.end();
  });
}
HttpDispatcher.prototype.dispatch = function(req, res) {
  var parsedUrl = require('url').parse(req.url, true);
  if(parsedUrl.pathname.indexOf(this.staticFolderPrefix) == 0) {
    this.staticListener(req, res);
	return;
  }
  var method = req.method.toLowerCase();
  if(this.listener[method][parsedUrl.pathname])
	this.listener[method][parsedUrl.pathname](req, res)
  else
	this.errorListener(req, res);
}

Per gestire le risorse statiche aggiungiamo una nuova proprietà privata staticFolderPrefix (valorizzata di default a "/static"), il metodo setter, il listener scritto ad hoc per le risorse statiche e un ulteriore controllo all'interno della funzione dispatch.

L'unica modifica che richiede un approfondimento è la definizione della funzione staticListener che grazie ai moduli path, fs e mime, è in grado di leggere il contenuto del file e scriverlo in formato binario all'interno della response inviando anche il mime-type corretto. In caso di file statico non trovato viene invocata il listener di errore.

Conclusioni

La definizione del modulo è completata. Da ora abbiamo a disposizione un dispatcher in grado di differenziare sulla base dell'URL e del metodo HTTP le diverse chiamate degli utenti. Nei prossimi articoli daremo per assodato questo modulo e lo includeremo ovunque dovesse servire.

Ovviamente qualsiasi idea, commento e suggerimento sul modulo httpdispatcher (che ricordo è disponibile come modulo NPM) è ben accetto.

Ti consigliamo anche