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.
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.
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.
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.
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.
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.
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.
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);
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]);
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.
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.
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.
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.
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).
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') ]); )
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');)
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.
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.
Questa è praticamente una scelta personale, ma per aiutarti a scegliere quella giusta, ecco un elenco di pro e contro di ciascun approccio:
Professionisti:
Contro:
Professionisti:
Contro:
Professionisti:
Contro:
passo
funziona correttamenteCome 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.