Gestione della natura asincrona di Node.js

Node.js ti consente di creare applicazioni in modo rapido e semplice. Ma a causa della sua natura asincrona, potrebbe essere difficile scrivere codice leggibile e gestibile. In questo articolo ti mostrerò alcuni suggerimenti su come ottenerlo.


Callback Hell o the Pyramid of Doom

Node.js è costruito in un modo che ti obbliga a utilizzare le funzioni asincrone. Ciò significa callback, richiamate e ancora più callback. Probabilmente hai visto o anche scritto te stesso pezzi di codice come questo:

app.get ('/ login', function (req, res) sql.query ('SELECT 1 FROM users WHERE name =?;', [req.param ('username')], funzione (errore, righe)  if (error) res.writeHead (500); return res.end (); if (rows.length < 1)  res.end('Wrong username!');  else  sql.query('SELECT 1 FROM users WHERE name = ? && password = MD5(?);', [ req.param('username'), req.param('password') ], function (error, rows)  if (error)  res.writeHead(500); return res.end();  if (rows.length < 1)  res.end('Wrong password!');  else  sql.query('SELECT * FROM userdata WHERE name = ?;', [ req.param('username') ], function (error, rows)  if (error)  res.writeHead(500); return res.end();  req.session.username = req.param('username'); req.session.data = rows[0]; res.rediect('/userarea'); );  );  ); );

Questo è in realtà uno snippet di una delle mie prime app Node.js. Se hai fatto qualcosa di più avanzato in Node.js probabilmente capisci tutto, ma il problema qui è che il codice si sta spostando a destra ogni volta che usi una funzione asincrona. Diventa più difficile leggere e più difficile eseguire il debug. Fortunatamente, ci sono alcune soluzioni per questo pasticcio, quindi puoi scegliere quello giusto per il tuo progetto.


Soluzione 1: denominazione e modulazione della callback

L'approccio più semplice sarebbe quello di nominare ogni callback (che ti aiuterà a eseguire il debug del codice) e suddividere tutto il tuo codice in moduli. L'esempio di accesso sopra può essere trasformato in un modulo in pochi semplici passaggi.

La struttura

Iniziamo con una semplice struttura del modulo. Per evitare la situazione di cui sopra, quando dividi il casino in piccoli problemi, facciamo in modo che sia una classe:

var util = require ('util'); function Login (username, password) function _checkForErrors (error, rows, reason)  function _checkUsername (error, rows)  function _checkPassword (error, rows)  function _getData (error, rows)  function perform ()  this.perform = esegui;  util.inherits (Login, EventEmitter);

La classe è costruita con due parametri: nome utente e parola d'ordine. Guardando il codice di esempio, abbiamo bisogno di tre funzioni: una per verificare se il nome utente è corretto (_checkUsername), un altro per verificare la password (_checkPassword) e un altro per restituire i dati relativi all'utente (_getData) e notificare all'app che l'accesso è andato a buon fine. C'è anche un _checkForErrors helper, che gestirà tutti gli errori. Infine, c'è un eseguire funzione, che avvierà la procedura di login (ed è l'unica funzione pubblica nella classe). Finalmente, ereditiamo da EventEmitter per semplificare l'uso di questa classe.

The Helper

Il _checkForErrors la funzione controllerà se si è verificato un errore o se la query SQL non restituisce righe ed emette l'errore appropriato (con il motivo che è stato fornito):

function _checkForErrors (error, rows, reason) if (error) this.emit ('error', error); ritorna vero;  if (rows.length < 1)  this.emit('failure', reason); return true;  return false; 

Ritorna anche vero o falso, a seconda che si sia verificato un errore o meno.

Esecuzione del login

Il eseguire la funzione dovrà fare solo una operazione: eseguire la prima query SQL (per verificare se esiste il nome utente) e assegnare la callback appropriata:

function perform () sql.query ('SELECT 1 FROM users WHERE name =?;', [nome utente], _checkUsername); 

Presumo che tu abbia la tua connessione SQL accessibile a livello globale nel sql variabile (solo per semplificare, discutere se questa è una buona pratica va oltre lo scopo di questo articolo). E questo è tutto per questa funzione.

Verifica il nome utente

Il passaggio successivo consiste nel verificare se il nome utente è corretto e, in tal caso, attivare la seconda query, per verificare la password:

function _checkUsername (error, rows) if (_checkForErrors (error, rows, 'username')) return false;  else sql.query ('SELECT 1 FROM users WHERE name =? && password = MD5 (?);', [nomeutente, password], _checkPassword); 

Praticamente lo stesso codice del campione disordinato, ad eccezione della gestione degli errori.

Controllo della password

Questa funzione è quasi identica alla precedente, con la sola differenza che si chiama query:

function _checkPassword (error, rows) if (_checkForErrors (error, rows, 'password')) return false;  else sql.query ('SELECT * FROM userdata WHERE name =?;', [username], _getData); 

Ottenere i dati relativi all'utente

L'ultima funzione in questa classe otterrà i dati relativi all'utente (il passaggio facoltativo) e genererà un evento di successo con esso:

function _getData (error, rows) if (_checkForErrors (error, rows)) return false;  else this.emit ('success', rows [0]); 

Tocchi finali e utilizzo

L'ultima cosa da fare è esportare la classe. Aggiungi questa riga dopo tutto il codice:

module.exports = Login;

Questo renderà il Accesso classificare l'unica cosa che il modulo esporterà. Può essere usato in seguito in questo modo (assumendo che tu abbia nominato il file del modulo login.js ed è nella stessa directory dello script principale):

var Login = require ('./ login.js'); ... app.get ('/ login', funzione (req, res) var login = new Login (req.param ('username'), req.param ( 'password)); login.on (' errore ', funzione (errore) res.writeHead (500); res.end ();); login.on (' errore ', funzione (motivo) if (motivo == 'username') res.end ('Nome utente errato!'); else if (ragione == 'password') res.end ('Password errata!');); login.on (' success ', function (data) req.session.username = req.param (' username '); req.session.data = data; res.redirect (' / userarea ');); login.perform (); );

Ecco alcune righe di codice in più, ma la leggibilità del codice è aumentata, in modo piuttosto evidente. Inoltre, questa soluzione non utilizza alcuna libreria esterna, il che la rende perfetta se qualcuno di nuovo arriva al tuo progetto.

Questo è stato il primo approccio, passiamo alla seconda.


Soluzione 2: promesse

Usare le promesse è un altro modo per risolvere questo problema. Una promessa (come puoi leggere nel link fornito) "rappresenta l'eventuale valore restituito dal singolo completamento di un'operazione". In pratica, significa che puoi concatenare le chiamate per appiattire la piramide e rendere il codice più facile da leggere.

Useremo il modulo Q, disponibile nel repository NPM.

Q in poche parole

Prima di iniziare, permettimi di presentarti la Q. Per le classi statiche (moduli), utilizzeremo principalmente il Q.nfcall funzione. Ci aiuta nella conversione di ogni funzione che segue il pattern di callback di Node.js (dove i parametri del callback sono l'errore e il risultato) a una promessa. È usato in questo modo:

Q.nfcall (http.get, opzioni);

È molto simile Object.prototype.call. Puoi anche usare il Q.nfapply che assomiglia Object.prototype.apply:

Q.nfapply (fs.readFile, ['filename.txt', 'utf-8']);

Inoltre, quando creiamo la promessa, aggiungiamo ogni passaggio con il poi (stepCallback) metodo, cattura gli errori con catch (errorCallback) e finisci con fatto().

In questo caso, dal momento che il sql oggetto è un'istanza, non una classe statica, dobbiamo usare Q.ninvoke o Q.npost, che sono simili a quanto sopra. La differenza è che passiamo il nome dei metodi come una stringa nel primo argomento e l'istanza della classe con cui vogliamo lavorare come seconda, per evitare che il metodo sia unbinded dall'istanza.

Preparare la promessa

La prima cosa da fare è eseguire il primo passo, usando Q.nfcall o Q.nfapply (usa quello che ti piace di più, non c'è alcuna differenza sotto):

var Q = require ('q'); ... app.get ('/ login', function (req, res) Q.ninvoke ('query', sql, 'SELECT 1 FROM users WHERE name =?;', [ req.param ('username')]));

Notare la mancanza di un punto e virgola alla fine della riga: le chiamate di funzione verranno concatenate in modo che non possano essere presenti. Stiamo solo chiamando il sql.query come nell'esempio disordinato, ma omettiamo il parametro callback - è gestito dalla promessa.

Verifica il nome utente

Ora possiamo creare il callback per la query SQL, sarà quasi identico a quello dell'esempio "pyramid of doom". Aggiungi questo dopo il Q.ninvoke chiamata:

.then (function (rows) if (rows.length < 1)  res.end('Wrong username!');  else  return Q.ninvoke('query', sql, 'SELECT 1 FROM users WHERE name = ? && password = MD5(?);', [ req.param('username'), req.param('password') ]);  )

Come puoi vedere stiamo allegando il callback (il prossimo passo) usando il poi metodo. Inoltre, nella richiamata omettiamo il errore parametro, perché in seguito cattureremo tutti gli errori. Stiamo controllando manualmente, se la query ha restituito qualcosa, e in tal caso stiamo restituendo la prossima promessa da eseguire (di nuovo, nessun punto e virgola a causa del concatenamento).

Controllo della password

Come con l'esempio di modularizzazione, il controllo della password è quasi identico al controllo del nome utente. Questo dovrebbe andare subito dopo l'ultimo poi chiamata:

.then (function (rows) if (rows.length < 1)  res.end('Wrong password!');  else  return Q.ninvoke('query', sql, 'SELECT * FROM userdata WHERE name = ?;', [ req.param('username') ]);  )

Ottenere i dati relativi all'utente

L'ultimo passo sarà quello in cui inseriremo i dati degli utenti nella sessione. Ancora una volta, la richiamata non è molto diversa dall'esempio disordinato:

.then (function (rows) req.session.username = req.param ('username'); req.session.data = rows [0]; res.rediect ('/ userarea');)

Controllo degli errori

Quando si usano le promesse e la libreria Q, tutti gli errori sono gestiti dal set di callback usando il catturare metodo. Qui, stiamo inviando l'HTTP 500 indipendentemente dall'errore, come negli esempi sopra:

.catch (function (error) res.writeHead (500); res.end ();) .done ();

Dopodiché, dobbiamo chiamare il fatto metodo per "assicurarsi che, se un errore non viene gestito prima della fine, verrà ripescato e segnalato" (dal README della libreria). Ora il nostro bel codice appiattito dovrebbe assomigliare a questo (e comportarsi proprio come quello disordinato):

var Q = require ('q'); ... app.get ('/ login', function (req, res) Q.ninvoke ('query', sql, 'SELECT 1 FROM users WHERE name =?;', [ req.param ('username')]) .then (function (rows) if (rows.length < 1)  res.end('Wrong username!');  else  return Q.ninvoke('query', sql, 'SELECT 1 FROM users WHERE name = ? && password = MD5(?);', [ req.param('username'), req.param('password') ]);  ) .then(function (rows)  if (rows.length < 1)  res.end('Wrong password!');  else  return Q.ninvoke('query', sql, 'SELECT * FROM userdata WHERE name = ?;', [ req.param('username') ]);  ) .then(function (rows)  req.session.username = req.param('username'); req.session.data = rows[0]; res.rediect('/userarea'); ) .catch(function (error)  res.writeHead(500); res.end(); ) .done(); );

Il codice è molto più pulito e ha comportato meno riscrittura rispetto all'approccio di modularizzazione.


Soluzione 3: Libreria Step

Questa soluzione è simile alla precedente, ma è più semplice. Q è un po 'pesante, perché implementa l'intera idea di promesse. La libreria Step è lì solo allo scopo di appiattire l'inferno del callback. È anche un po 'più semplice da usare, perché basta chiamare l'unica funzione che viene esportata dal modulo, passare tutti i callback come parametri e usare Questo al posto di ogni richiamata. Quindi l'esempio disordinato può essere convertito in questo, usando il modulo Step:

var step = require ('step'); ... app.get ('/ login', function (req, res) step (function start () sql.query ('SELECT 1 FROM users WHERE name =?;', [req.param ('username')], this);, function checkUsername (error, rows) if (error) res.writeHead (500); return res.end (); if (rows.length < 1)  res.end('Wrong username!');  else  sql.query('SELECT 1 FROM users WHERE name = ? && password = MD5(?);', [ req.param('username'), req.param('password') ], this);  , function checkPassword(error, rows)  if (error)  res.writeHead(500); return res.end();  if (rows.length < 1)  res.end('Wrong password!');  else  sql.query('SELECT * FROM userdata WHERE name = ?;', [ req.param('username') ], this);  , function (error, rows)  if (error)  res.writeHead(500); return res.end();  req.session.username = req.param('username'); req.session.data = rows[0]; res.rediect('/userarea');  ); );

Lo svantaggio qui è che non esiste un gestore di errori comune. Sebbene tutte le eccezioni lanciate in un callback siano passate al successivo come primo parametro (quindi lo script non andrà giù a causa dell'eccezione non rilevata), avere un gestore per tutti gli errori è conveniente per la maggior parte del tempo.


Quale scegliere?

Questa è praticamente una scelta personale, ma per aiutarti a scegliere quella giusta, ecco un elenco di pro e contro di ciascun approccio:

La modularizzazione:

Professionisti:

  • Nessuna libreria esterna
  • Aiuta a rendere il codice più riutilizzabile

Contro:

  • Più codice
  • Un sacco di riscrittura se stai convertendo un progetto esistente

Promesse (Q):

Professionisti:

  • Meno codice
  • Solo una piccola riscrittura se applicata a un progetto esistente

Contro:

  • Devi usare una libreria esterna
  • Richiede un po 'di apprendimento

Step Library:

Professionisti:

  • Facile da usare, nessun apprendimento richiesto
  • Praticamente copia e incolla se si converte un progetto esistente

Contro:

  • Nessun gestore di errori comune
  • Un po 'più difficile da indentare passo funziona correttamente

Conclusione

Come puoi vedere, la natura asincrona di Node.js può essere gestita e l'inferno di callback può essere evitato. Uso personalmente l'approccio di modularizzazione, perché mi piace avere il mio codice ben strutturato. Spero che questi suggerimenti ti aiutino a scrivere il tuo codice più leggibile e a eseguire il debug degli script più facilmente.