Node Js

Dal momento in cui Node.js è stato presentato al modo, si è visto un dibattito fra fautori e critici. Il dibattito ancora continua, e potrebbe non finire mai. A prescindere di quanto sia difficile scrivere codice sicuro in Node, e quanto sia facile scrivere codice asincrono e concorrente, la piattaforma è già stata rilasciata da un pò ed è stata usata per realizzare un gran numero di servizi web, sofisticati e robusti.

Questi servizi web scalano bene e hanno dato prova della loro stabilità durante la loro intera permanenza su internet.

Tuttavia, come ogni altra piattaforma, Node.js è vulnerabile ai problemi introdotti dagli sviluppatori stessi. Alcuni di questi errori degradano le performance, mentre altri rendono Node.js inutilizzabile per qualunque cosa si stia cercando di ottenere. In questo articolo, daremo un’occhiata ai 10 errori più comuni che gli sviluppatori nuovi a Node.js spesso commettono e come poterli evitare.

Errore #1: Bloccare l’event loop

Javascript, così come nei Browser anche in Node.js, fornisce un ambiente single-thread. Questo significa che nessuna delle parti della nostra app gira in parallelo; invece, la concorrenza è raggiunta attraverso la gestione asincrona delle operazioni I/O. Per esempio, una richiesta da Node.js verso un Database, per richiamare dei documenti, è quello che permette a Node.js di concentrarsi su altre parti dell’applicazione.

// Si cerca di prendere l'utente dal Database. Node.js esegue altre parti di codice nel mentre questa funzione è invocata e si attende una risposta.
db.User.get(userId, function(err, user) {
	// ...
})


Si noti però come un singolo pezzo di codice CPU-intensive in un’istanza di Node.js con migliaia di client connessi possa bloccare l’event-loop, facendo aspettare tutti i client.
Codice di questo tipo include ad esempio il riordinamento di un grande array, un ciclo molto lungo e così via.

function sortUsersByAge(users) {
	users.sort(function(a, b) {
		return a.age < b.age ? -1 : 1
	})
}

Invocare questa funzione può andare bene se l’array passato come parametro è piccolo. Con un array veramente grande, avrà un terribile impatto sulle performance generali del servizio. Se questa funzione è parte di qualcosa che ha un impatto “locale”, come ad esempio un programma che il client esegue una tantum sul PC, allora può anche andar bene far eseguire il codice in maniera sincrona. Se invece si tratta di un’istanza di un servizio che serve migliaia di utenti, questo paradigma può essere fatale.

Alcune soluzioni possono essere ad esempio, se l’array viene recuperato dal DB, far in modo che venga recuperato già ordinato. Oppure delegare il compito di riordinamento a un nuovo worker, spawnato per l’occasione o dormiente e in attesa di processare calcoli specifici di questo tipo. Come si può vedere non c’è una soluzione universale per questo tipo di problema. L’importante è non far eseguire del lavoro CPU-intensive sulle istanze di Node.js che stanno in prima linea nella comunicazione con i client.

Errore #2: Invocare una callback più volte a causa dei return

Javascript da quando esiste ha sempre fatto affidamento sul concetto di callbacks. Nei web browser, gli eventi sono gestiti passando le referenze a delle funzioni (spesso anonime) che fungono da callbacks. In Node.js, le callbacks sono sempre state l’unico modo per far comunicare fra di loro elementi asincroni nel codice (adesso esistono anche le Promise). Un problema comune legato all’utilizzo delle callbacks è quello di chiamarle più volte. Tipicamente, una funzione fornita da un package per far qualcosa in modo asincrono è progettata per avere come ultimo argomento una funzione, che viene chiamata quando il task asincrono è stato completato:

module.exports.verifyPassword = function(user, password, done) {
	if(typeof password !== 'string') {
		done(new Error('password should be a string'));
		return;
	}
 
	computeHash(password, user.passwordHashOpts, function(err, hash) {
		if(err) {
			done(err);
			return;
		}
 
		done(null, hash === user.passwordHash);
	});
}

Notare come ci sia un’istruzione di return ogni volta che “done” è chiamata. Questo perchè chiamare la callback non termina automaticamente l’esecuzione della funzione corrente. Se il primo return viene commentato, passando una password non-stringa a questa funzione farà si che “computeHash” venga ugualmente chiamata! Portndo a un comportamento inaspettato.

Alcuni svilupaptori Node.js adottano la pratica di inserire l’istruzione di return subito prima della chiamata alla callback:

if (err)
    return done(err);

In molte funzioni asincrone, il valore restituito dal return non ha un significato, quindi questo approccio rende facile la soluzione per questo tipo di problemi.

Errore #3: Callbacks fittamente annidate

Il problema di avere delle callbacks fittamente annidate viene comunemente riconosciuto col nome di “callbacks hell”. Non è un problema Node.js di per se. Ma può portare alla perdita di controllo sull’app.

function handleLogin(..., done) {
	db.User.get(..., function(..., user) {
		if(!user) {
			return done(null, 'failed to log in');
		}
		utils.verifyPassword(..., function(..., okay) {
			if(okay) {
				return done(null, 'failed to log in');
			}
			session.login(..., function() {
				done(null, 'logged in');
			})
		})
	})
}

Più complesso è il task che si vuole svolgere, più difficile sarà districarsi fra le callbacks. Il codice inoltre risulta difficile da leggere e da mantenere.
Una delle soluzioni più eleganti è quella di usare un pacchetto per Node chiamato Async.js, che bene affronta questo pattern di Javascript (Oppure usare le nuove Promise).

function handleLogin(done) {
	async.waterfall([
		function(done) {
			db.User.get(..., done)
		},
		function(user, done) {
			if(!user) {
			return done(null, 'failed to log in');
			}
			utils.verifyPassword(..., function(..., okay) {
				done(null, user, okay)
			})
		},
		function(user, okay, done) {
			if(okay) {
				return done(null, 'failed to log in');
			}
			session.login(..., function() {
				done(null, 'logged in');
			})
		}
	], function() {
		// ...
	})
}

Similmente al metodo a cascata, “waterfall”, ci sono molti altri metodi offerti da Async.js.

Errore #4: Aspettarsi che le callbacks girino in modo Sincrono

La programmazione asincrona con le callbacks potrà non essere qualcosa di unico attribuibile a Javascript e Node.js, ma sicuramente è la responsabile della loro popolarità. Con altri linguaggi di programmazione, siamo accomunati dalla prevedibile esecuzione sequenziale delle istruzioni. In Javascript non è così:

function testTimeout() {
	console.log("inizio")
	setTimeout(function() {
		console.log("fatto!")
	}, duration * 1000)
	console.log("in Attesa..")
}

Se eseguiamo la funzione, il risultato che avremo sarà, la stampa della scritta “inizio”, dopo “in Attesa..”, seguito dal messaggio “fatto!” dopo circa un secondo. Due parole, Programmazione asincrona.

Errore #5: Assegnare ad “exports” invece di “module.exports”

Node.js tratta ogni file come fosse un piccolo modulo isolato. Se il vostro package ha due file, “a.js” e “b.js”, per far in modo che “b” acceda alle funzionalità di “a”, bisogna che “a” esporti tali funzionalità assegnandole all’oggetto exports.

// a.js
exports.verificaPassword = function(user, pass, done){ ... }

Quando questo viene fatto, chiunque richieda “a” riceverà un oggetto con la proprietà “verificaPassword”.

// b.js
require('./a.js'); // { verificaPassword: function(user, pass, done) { ... } }

E se volessimo esportare la funzione direttamente e non come proprietà di qualche oggetto? Possiamo sovrascrivere exports per farlo, ma non dobbiamo trattarlo come variabile globale:

// a.js
module.exports = function(user, password, done) { ... }

Notare come stiamo trattando “exports”, come proprietà dell’oggetto modulo. La distinzione qui fra “module.exports” e “exports” è molto importante, ed è spesso causa di frustrazione fra i nuovi sviluppatori Node.

Errore #6: Lanciare Errori da dentro le callbacks

Javascript ha la nozione di Eccezioni. Mimando la sintassi di altri linguaggi di programmazione tradizionali. Javascript può lanciare “throw” e catturare “catch” eccezioni nei blocchi try-catch:

function slugifyUsername(username) {
	if(typeof username === 'string') {
		throw new TypeError('expected a string username, got '+(typeof username))
	}
	// ...
}
 
try {
	var usernameSlug = slugifyUsername(username)
} catch(e) {
	console.log('Oh no!')
}

Tuttavia, try-catch non si comporta come ci si potrebbe aspettare in situazioni asincrone. Per esempio, se volessimo catturare l’eccezione per un grosso blocco di codice di attività asincrone, non necessariamente potrebbe funzionare:

try {
	db.User.get(userId, function(err, user) {
		if(err) {
			throw err;
		}
		// ...
		usernameSlug = slugifyUsername(user.username);
		// ...
	})
} catch(e) {
	console.log('Oh no!');
}

Se la chiamata “db.User.get” è asincrona, lo scope del try-catch è fuori dal contesto della callback.
Motivo per cui spesso si tende ad usare il pattern (err, … ), per le funzioni di callback, in cui il primo argomento è proprio l’eventuale errore.

Errore #7: Assumere che i numeri siano di tipo Integer

I numeri in Javascript sono del tipo “Floating”. Non esiste il tipo intero.
Dal momento che i numeri in virgola mobile possono rappresentare un intero solo fino a un certo valore, eccedendo quel valore in un qualunque tipo di calcolo si compromette tutto quanto.
Per quanto strano possa sembrare, la linea che segue restituisce “true” in Node.js:

Math.pow(2, 53)+1 === Math.pow(2, 53)

Sfortunatamente, le stranezze con i numeri in Javascript non finisco qui. Nonostante i numeri siano in virgola mobile, gli operatori che funzionano bene con gli interi, funzionano bene anche qui:

5 % 2 === 1 // true
5 >> 1 === 2 // true

Tuttavia, al contrario delle operazioni aritmetiche, gli operatori bitwise e shift funzionano solo sugli ultimi 32 bit di questi grossi numeri “interi”. Per esempio:

Math.pow(2, 53) / 2 === Math.pow(2, 52) // true
Math.pow(2, 53) >> 1 === 0 // true
Math.pow(2, 53) | 1 === 1 // true

Raramente magari si avrà il bisogno di lavorare con grossi numeri. Ma se mai si dovesse averne bisogno, ci sono svariate librerie che implementano le operazioni matematiche più comuni con grossi numeri, come node-bigint.

Errore #8: Ignorare i vantaggi delle Streaming API

Immaginiamo di voler costruire un piccolo web server che funga da proxy e che risponda alle richieste prendendo il contenuto da altri web server. Come esempio, si può pensare a questo web server che serve le immagini di Gravatar.

var http = require('http')
var crypto = require('crypto')
 
http.createServer()
.on('request', function(req, res) {
	var email = req.url.substr(req.url.lastIndexOf('/')+1)
	if(!email) {
		res.writeHead(404)
		return res.end()
	}
 
	var buf = new Buffer(1024*1024)
	http.get('http://www.gravatar.com/avatar/'+crypto.createHash('md5').update(email).digest('hex'), function(resp) {
		var size = 0
		resp.on('data', function(chunk) {
			chunk.copy(buf, size)
			size += chunk.length
		})
		.on('end', function() {
			res.write(buf.slice(0, size))
			res.end()
		})
	})
})
.listen(8080)

In questo particolare problema, stiamo recuperando l’immagine da Gravatar, leggendola in un Buffer, e dopo rispondendo alla richiesta.
Questa non è una cosa così terribile da fare, dal momento che le immagini di Gravatar sono piccole. Tuttavia, immaginiamo che le dimensioni del contenuto che stiamo proxando siano migliaia di megabytes, questo comporterebbe l’avere un buffer di altrettanti Mb. Un approccio migliore è senz’altro questo:

http.createServer()
.on('request', function(req, res) {
	var email = req.url.substr(req.url.lastIndexOf('/')+1)
	if(!email) {
		res.writeHead(404)
		return res.end()
	}
 
	http.get('http://www.gravatar.com/avatar/'+crypto.createHash('md5').update(email).digest('hex'), function(resp) {
		resp.pipe(res)
	})
})
.listen(8080)

Qui, prendiamo l’immagine e trasportiamo (pipe) la risposta al client. In nessun momento stiamo salvando l’immagine in un buffer e occupando memoria, prima di servirla.

Errore #9: Usare console.log per il Debug

In Node.js, console.log ti permette di stampare sulla console qualsiasi cosa. Accetta un numero arbitrario di argomenti e li stampa separati da uno spazio. Ci sono svariate ragioni sul perchè gli sviluppatori potrebbero essere tentati nell’usarlo per effettuare il Debug della loro app. Tuttavia, si raccomanda fortemente di non usarlo in produzione. Piuttosto usate una delle molte librerie pensate appositamente per questo. Ad esempio il modulo debug.

Pacchetti come questi garantiscono modi convenienti per abilitare e disabilitare certe linee di debug quando si avvia l’applicazione.
Ad esempio, si può evitare che ogni messaggio di debug venga pubblicato sulla console:

// app.js
var debug = require('debug')('app')
debug('Hello, %s!', 'world')

oppure fare in modo che effettivamente ogni messaggio di debug venga mostrato:

DEBUG=app node app.js

Errore #10: Non usare programmi Supervisor

A prescindere di dove il vostro codice Node.js sia in esecuzione, in produzione o in un ambiente di sviluppo locale. Un programma di supervisioning che possa orchestrare l’esecuzione del programma è uno strumento estremamente utile da avere.
Una pratica comune fra gli sviluppatori moderni che implementano applicazioni di ogni tipo, è quella di lasciar fallire il codice se si verifica un errore inaspettato, così da lasciar il compito al supervisor di riavviare il processo in pochi secondi. I benefici del supervisore non si limitano solo al riavvio dei programmi che crashano. Ma permettono anche di riavviare programmi quando qualche file viene modificato, oppure di monitorare l’uso delle risorse da parte di questi processi o loggare ogni messaggio dell’app.

Ci sono svariati programmi che assolvono questo compito su Node.js, eccone elencati alcuni:

pm2
– forever
– nodemon
– supervisor

Conclusioni

Come si può intuire, alcuni di questi problemi possono causare effetti devastanti sui nostri programmi. Alcuni potrebbero essere banalmente la causa di inutili frustrazioni nello sviluppo di app banali.
Molto è stato fatto per rendere Node.js estremamente semplice per i nuovi arrivati.
Spero che questa guida possa aiutare chi è alle prime armi con Node.js e che possa aiutare a sviluppare applicazioni più stabili ed efficenti per noi tutti.