In questa serie di articoli si vuole introdurre il lettore a Node.js.
Attraverso esempi di codice inframezzati da brevi spiegazioni si approfondiranno vari argomenti inerenti allo sviluppo di applicazioni con Node.js.
In questo articolo si parte da una breve presentazione per poi procedere passo passo alla creazione di un progetto di applicazione web che esponga una API.
Node.js
Node.js è un runtime per Javascript, basato sul motore di renderizzazione di Chrome (v8), che consente di creare applicazioni server per protocollo TCP-IP.
L’uso prevalente è la creazione di applicazioni web.
Node.js: Codice server side
Node.js consente di scrivere codice server in Javascript. Le API necessarie per accedere alle risorse che un server può mettere a disposizione sono organizzate in moduli importabili.
Node.js viene distribuito con alcuni moduli built-in, come http per la creazione di applicazioni server web e fs per l’accesso al file system del server.
Le risorse normalmente disponibili nel browser, come l’oggetto Window o il DOM, non sono invece presenti nella distribuzione di base.
Ai moduli presenti nella distribuzione è possibile aggiungere altri moduli, di terze parti o sviluppati in proprio, che estendono le funzionalità di Node.js e implementano la logica applicativa.
Cosa può fare Node.js?
Sostanzialmente tutte le applicazioni con uso intensivo di risorse I/O potrebbero essere realizzate con successo in Node.js. Per esempio: Applicazioni web, API su database ad oggetti, microservizi su piattaforme cloud, applicazioni in architettura serverless, IoT.
- API e applicazioni web. L’applicazione si deve connettere spesso al disco rigido del server o al database, parsando e formattando il dato in risposta. Node.js riesce a garantire un basso consumo di risorse perché utilizza il tempo in cui una richiesta è bloccata in attesa di operazioni I/O per servire altre richieste.
- Microservizi e architetture serverless. Node.js ha un rapido startup e non consuma molte risorse. Può essere impiegato per offrire servizi specifici che possano essere scalati orizzontalmente in autonomia.
- IoT. Node.js implementa un server TCP IP generico che può essere programmato per interfacciarsi con qualsiasi dispositivo IoT ma è talmente leggero che può essere flashato in microcontrollori per creare dispositivi server di controllo e gestione di una rete di devices.
- Google Home, Amazon Alexa. Node.js può fare da backend per app pubblicate su assistenti virtuali, dispositivi mobili e persino automobili.
- Applicazioni desktop. Node.js è impiegato in applicazioni ibride, su base chromium, come runtime del codice javascript. Sono realizzate in questo modo le applicazioni basate sul framework Electron.
Quando Node.js non è indicato
- Applicazioni che facciano un alto uso di CPU. Node.js è single thread, una richiesta che comporti operazioni di calcolo lunghe ed intense bloccano il server impedendo a qualsiasi utente di collegarsi al server.
- Programmazione parallela. Sempre per la sua architettura single thread Node.js non può implementare algoritmi paralleli.
Npm
Una volta installato Node.js si ha a disposizione NPM, un gestore di pacchetti che permette:
- di installare e/o distribuire moduli aggiuntivi
- di installare eventuali dipendenze
- di aggiornare i moduli o Node stesso
- eseguire script specifici, come lo startup di un progetto o l’esecuzione dei test
Alcuni esempi di applicazioni Node.js
Un progetto molto semplice può essere costituito da un unico file javascript che può essere messo in esecuzione tramite node:
$ echo "console.log('Hello World');" > esempio.js
$ node esempio.js
Hello World
Il programma è molto semplice, stampa ‘Hello World’ a console e poi esce.
Il successivo esempio invece implementa un server tcp ip che ripete i messaggi che riceve:
const net = require('net');
const server = net.createServer(
function(socket) {
socket.write('Echo server \r\n');
socket.pipe(socket);
}
);
server.listen(1337, '127.0.0.1');
In questo caso viene importato il modulo net, viene creato un oggetto server al quale viene associato una callback che si occupa di gestire la comunicazione con il client. Chiamando il metodo listen si entra nell’event-loop e l’applicazione rimane in ascolto sulla porta.
Codice non bloccante
Le applicazioni Node.js sono single-threaded, il codice viene eseguito quando all’interno dell’event-loop si scatena un evento a cui è associato una callback. Durante l’esecuzione di una callback eventuali altri eventi saranno messi in coda e gestiti non appena il thread principale torna disponibile. La modalità di gestione delle chiamate di sistema (eventi) con callback è chiamata non blocking I/O.
Con un singolo thread un server può servire molti client nello scenario in cui molto del tempo impiegato per rispondere ad una richiesta è impiegato per operazioni I/O. Molti moduli di Node mettono a disposizione anche API sincrone (bloccanti) oltre al rispettivo equivalente asincrono. Il codice sincrono è più semplice da scrivere dell’equivalente asincrono ma le prestazioni sono decisamente inferiori.
Versione sincrona:
const fs = require('fs');
const data = fs.readFileSync('/file.md');
Versione asincrona:
const fs = require('fs');
fs.readFile('/file.md', (err, data) => {
if (err) throw err;
});
Http
Il modulo Http implementa un server web a cui è possibile associare eventi a url specifici.
var http = require('http');
var server = http.createServer(function(req, res) {
res.end('Hello World');
});
server.listen(8080, '127.0.0.1');
console.log('Server listening');
Il codice risponderà a qualsiasi chiamata al server con Hello world. L’esempio è molto semplice, ricorda il server TCP e non fa alcun discrimine circa l’url che il server riceve.
const http = require('http')
const server = http.createServer(function(req, res) {
res.writeHead(200, {'Content-Type': 'text/html'});
var url = req.url;
switch(url) {
case "/ciao":
res.write('
Mondo
‘); break; case “/hello”: res.write(‘
World
‘); break; default: res.write(‘
Chi sei?
‘); } res.end(); }); server.listen(8080, ‘127.0.0.1’);
Questo esempio invece analizza l’url e personalizza la risposta.
Express.js
I precedenti esempi di server http, seppur funzionanti, sono molto primitivi. Sarebbe senz’altro possibile estendere la logica di questi esempi introducendo un routing più sofisticato ed un sistema di templating HTML per le risposte. Express.js è un framework per Node.js che implementa queste ed altre funzionalità avanzate come filtri, serializzatori di richieste e risposte ed un flusso di gestione delle richieste ispirato alle pipe. Installabile come modulo aggiuntivo, risulta più comodo utilizzarlo con la sua cli in quanto è in grado di generare applicazioni tramite un wizard che permette la scelta tra diversi template di progetto. È possibile, per esempio, generare applicazioni specifiche per API. Express non installerà il motore di templating.
// app.js
const express = require('express')
const app = express()
const port = process.env.HTTP_PORT || 3000
app.use(function(req, res, next) {
console.log(req.url);
next();
});
app.get('/', (req, res) => {
res.send('Hello World!');
})
app.listen(port, () => console.log(`Example app listening on port ${port}!`));
Per eseguirlo:
$ node app.js
Example app listening on port 3000
La variabile app è l’istanza di express.
app.get indica che l’handler che si va ad aggiungere opererà quando verrà fatta una richiesta di tipo get. Segue l’url e una funzione fat arrow che riceve richiesta e oggetto per rispondere.
app.listen avvia l’event loop e associa una callback in caso che la chiamata abbia successo.
app.use è un esempio di middleware. Il middleware è una callback che partecipa al flusso di generazione della risposta ad una richiesta e può essere utilizzato sia per effettuare elaborazioni necessarie a tutte le richieste (p.e. il parsing di un post in formato JSON), sia per interrompere il flusso nel caso si renda necessario (p.e. la richiesta di un utente non autorizzato). I parametri in ingresso di una middleware sono tre: la richiesta, l’handler di riposta nel caso si voglia interrompere il flusso e rispondere immediatamente e un handler di avanzamento del processo: next.
Separazione dei compiti
// app.js
const express = require('express')
const path = require('path')
const app = express()
const userRoutes = require('./routes/user')
app.use(function(req, res, next) {
console.log(req.url);
next();
});
app.use('/users', userRouters);
module.exports = app;
// routes/user.js
const express = require('express')
const router = express.Router()
const users = [
{
id: 1,
nome: 'John Doe'
},
{
id: 2,
nome: 'Jenny Doe'
},
];
router.get('/', function(req, res) {
res.send(users);
});
router.get('/:id', function(req, res) {
const id = req.params.id;
const mayUser = users.filter(u => { return u.id === id });
if (mayUser.length > 0) {
res.send(mayUser[0]);
} else {
res.status(404).send('Not Found');
}
});
module.exports = router;
// bin/www
const app = require('../app');
const http = require('http');
const port = 3000;
app.set('port', port);
const server = http.createServer(app);
server.listen(port);
$ node app.js
Questo esempio mostra l’usuale separazione dei compiti dei vari listati di un programma Node.js.
app.js è il file di configurazione e wiring generale dell’applicazione. Il routing e la logica applicativa è implementata nei file di routing, assimilabili a controller in una struttura MVC. Il file di routing esporta così il router configurato con le varie callback, nel file app.js il router viene associato ad un Url di base. L’app express viene quindi importato nello script www che è l’entry point del programma.
Il programma può essere avviato tramite il comando:
$ node bin/www
Se il progetto è stato creato con il cli express esiste un task di avvio in package.json in modo da poter usare direttamente npm:
$ npm start
In sintesi
Node.js è un runtime in grado di eseguire codice javascript nel server. La sua leggerezza ed la sua architettura lo rendono la piattaforma ideale per sviluppare applicazioni che facciano un uso intenso di risorse I/O e ben si presta ad essere eseguito in ambienti cloud o con poche risorse.
La sua popolarità ha portato molte aziende a fornire servizi specifici o la possibilità di utilizzarlo per interfacciarsi con i propri, p.e. Heroku, Google e Amazon.
Questo non dovrebbe far dimenticare che Node.js non è adatto per applicazioni di calcolo intensivo e/o parallelo, pertanto il suo impiego va valutato e non può essere dato per scontato.