Node.js vă permite să creați aplicații rapid și ușor. Dar, datorită naturii sale asincrone, este greu să scriem cod lizibil și ușor de gestionat. În acest articol vă voi arăta câteva sfaturi despre cum să realizați acest lucru.
Node.js este construit într-un mod care vă obligă să utilizați funcții asincrone. Asta înseamnă apeluri telefonice, apeluri telefonice și chiar mai multe apeluri telefonice. Probabil ați văzut sau chiar ați scris niște bucăți de cod ca acesta:
app.get ('/ login', funcția (req, res) sql.query ('SELECT 1 FROM users WHERE name =?;', dacă (eroare) res.writeHead (500); retur res.end (); dacă (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'); ); ); ); );
Acesta este de fapt un fragment direct de la una dintre primele mele aplicații Node.js. Dacă ați făcut ceva mai avansat în Node.js, probabil că înțelegeți totul, dar problema este că codul se mișcă spre dreapta de fiecare dată când utilizați o funcție asincronă. Ea devine mai greu de citit și mai greu de depanat. Din fericire, există câteva soluții pentru această mizerie, astfel încât să puteți alege cea potrivită pentru proiectul dvs..
Cea mai simplă abordare ar fi numirea fiecărui apel invers (care vă va ajuta să depanați codul) și împărțiți tot codul în module. Exemplul de logare de mai sus poate fi transformat într-un modul în câțiva pași simpli.
Să începem cu o structură simplă a modulelor. Pentru a evita situația de mai sus, atunci când tocmai ați împărțit mizeria în mese mai mici, hai să fie o clasă:
var util = cere ('util'); (nume, parolă) funcția _checkForErrors (eroare, rânduri, motiv) funcția _checkUsername (eroare, rânduri) funcția _checkPassword this.perform = perform; util.inherits (Login, EventEmitter);
Clasa este construită cu doi parametri: nume de utilizator
și parola
. Privind exemplul de cod, avem nevoie de trei funcții: una pentru a verifica dacă numele de utilizator este corect (_checkUsername
), altul pentru a verifica parola (_checkPassword
) și încă o dată pentru a returna datele legate de utilizator (_Obțineți date
) și notificați aplicația că datele de conectare au reușit. Este deasemenea o _checkForErrors
helper, care va rezolva toate erorile. În cele din urmă, există a a executa
care va începe procedura de conectare (și este singura funcție publică din clasă). În cele din urmă, moștenim EventEmitter
pentru a simplifica utilizarea acestei clase.
_checkForErrors
funcția va verifica dacă a apărut o eroare sau dacă interogarea SQL nu returnează rânduri și emite eroarea corespunzătoare (cu motivul care a fost furnizat):
funcția _checkForErrors (eroare, rânduri, motiv) if (eroare) this.emit ("eroare", eroare); return true; dacă (rows.length < 1) this.emit('failure', reason); return true; return false;
Se întoarce, de asemenea Adevărat
sau fals
, în funcție de existența unei erori sau nu.
a executa
funcția va trebui să efectueze o singură operație: efectuați prima interogare SQL (pentru a verifica dacă există numele de utilizator) și asociați apelul corespunzător:
function execute () sql.query ('SELECT 1 FROM users WHERE nume =?;', [username], _checkUsername);
Presupun că aveți conexiunea dvs. SQL accesibilă la nivel global în sql
variabilă (doar pentru a simplifica, discutarea dacă este o practică bună depășește domeniul de aplicare al acestui articol). Și asta este pentru această funcție.
Următorul pas este să verificați dacă numele de utilizator este corect și, dacă este cazul, să încercați a doua interogare - pentru a verifica parola:
_checkForErrors (eroare, rânduri, 'username')) return false; altceva sql.query ('SELECT 1 FROM users WHERE nume =? && password = MD5 (?);', [username, password], _checkPassword);
Destul de același cod ca și în proba murdară, cu excepția manipulării erorilor.
Această funcție este aproape exact aceeași cu cea anterioară, singura diferență fiind interogarea numită:
_checkForErrors (eroare, rânduri, "parola")) return false; altceva sql.query ('SELECT * FROM userdata WHERE nume =?;', [username], _getData);
Ultima funcție din această clasă va primi datele referitoare la utilizator (pasul opțional) și va declanșa un eveniment de succes cu acesta:
funcția _getData (eroare, rânduri) if (_checkForErrors (eroare, rânduri)) return false; altceva this.emit ('succes', rânduri [0]);
Ultimul lucru pe care trebuie să-l faceți este să exportați clasa. Adăugați acest rând după tot codul:
module.exports = Conectare;
Acest lucru va face Logare
clasa singurul lucru pe care modulul îl va exporta. Acesta poate fi folosit mai târziu astfel (presupunând că ați denumit fișierul modulului login.js
și este în același director ca scriptul principal):
var login = solicita ('./ login.js'); ... app.get ('/ login', function (req, res) parola)); login.on ('eroare', funcție (eroare) res.writeHead (500); res.end ();); login.on (' == 'username') res.end ('Nume de utilizator greșit!'); altceva (motiv == 'parola') res.end (' succes ", funcția (data) req.session.username = req.param ('username'); req.session.data = data; res.redirect ('/ userarea');); );
Iată câteva linii de cod, dar citibilitatea codului a crescut, destul de evident. De asemenea, această soluție nu utilizează nici o bibliotecă externă, ceea ce îl face perfect dacă vine cineva nou la proiectul dvs..
Aceasta a fost prima abordare, să mergem la cea de-a doua.
Folosirea promisiunilor este o altă modalitate de a rezolva această problemă. O promisiune (așa cum puteți citi în link-ul furnizat) "reprezintă valoarea eventuală returnată de la finalizarea unică a unei operații". În practică, înseamnă că puteți să legeți apelurile pentru a aplatiza piramida și pentru a face codul mai ușor de citit.
Vom folosi modulul Q, disponibil în repozitoriul NPM.
Înainte de a începe, permiteți-mi să vă prezint Q. Pentru clasele statice (module), vom folosi în primul rând Q.nfcall
funcţie. Ne ajută în conversia fiecărei funcții după modelul de apel invers al Node.js (unde parametrii callback-ului sunt eroarea și rezultatul) la o promisiune. Se folosește astfel:
Q.nfcall (http.get, opțiuni);
Seamănă foarte mult Object.prototype.call
. De asemenea, puteți utiliza funcția Q.nfapply
care seamănă Object.prototype.apply
:
Q.nfapply (fs.readFile, ['nume fișier.txt', 'utf-8']);
De asemenea, atunci când creăm promisiunea, adăugăm fiecare pas cu apoi (stepCallback)
, prindeți erorile cu captura (errorCallback)
și termină cu Terminat()
.
În acest caz, din moment ce sql
obiectul este o instanță, nu o clasă statică, trebuie să o folosim Q.ninvoke
sau Q.npost
, care sunt similare celor de mai sus. Diferența este că vom trece numele metodei ca șir în primul argument și instanța clasei cu care dorim să lucrăm ca o a doua, pentru a evita ca metoda să fie UNBINDed de la instanță.
Primul lucru pe care trebuie să-l faceți este să executați primul pas, folosind Q.nfcall
sau Q.nfapply
(utilizați unul care vă place mai mult, nu există nici o diferență dedesubt):
var Q = solicita ('q'); ... app.get ('/ login', function (req, res) Q.ninvoke req.param ("nume de utilizator")]));
Observați lipsa unui punct și virgulă la sfârșitul liniei - apelurile funcționale vor fi înlănțuite astfel încât să nu poată fi acolo. Suntem doar de asteptare sql.query
ca în exemplul dezordonat, dar omitem parametrul de apel invers - este gestionat de promisiune.
Acum putem crea apelul invers pentru interogarea SQL, va fi aproape identic cu cel din exemplul "piramida doomului". Adăugați aceasta după Q.ninvoke
apel:
.apoi (funcția (rânduri) 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') ]); )
După cum vedeți, atașăm apelul de apel (pasul următor) folosind atunci
metodă. De asemenea, în apelul de apel am omite eroare
parametru, deoarece vom prinde toate erorile mai târziu. Verificăm manual, dacă interogarea a returnat ceva și, dacă este așa, vom întoarce următoarea promisiune care urmează să fie executată (din nou, nu există punct și virgulă din cauza legării).
Ca și în cazul exemplului de modulare, verificarea parolei este aproape identică cu verificarea numelui de utilizator. Acest lucru ar trebui să meargă imediat după ultimul atunci
apel:
.apoi (funcția (rânduri) if (rows.length < 1) res.end('Wrong password!'); else return Q.ninvoke('query', sql, 'SELECT * FROM userdata WHERE name = ?;', [ req.param('username') ]); )
Ultimul pas va fi cel în care punem datele utilizatorilor în sesiune. Încă o dată, apelul nu este mult diferit de exemplul murdar:
.apoi (funcția (rânduri) req.session.username = req.param ('username'); req.session.data = rânduri [0]; res.rediect ('/ userarea');)
Când se utilizează promisiunile și biblioteca Q, toate erorile sunt gestionate de setul de apel invers folosind captură
metodă. Aici, trimitem HTTP 500 numai indiferent de eroare, ca în exemplele de mai sus:
.captură (funcție (eroare) res.writeHead (500); res.end ();) .done ();
După aceea, trebuie să sunăm Terminat
metodă pentru a "asigura că, dacă o eroare nu este rezolvată înainte de sfârșit, va fi revocată și raportată" (din biblioteca README). Acum, codul nostru frumos aplatizat ar trebui să arate așa (și să se comporte ca și cel dezordonat):
var Q = solicita ('q'); ... app.get ('/ login', function (req, res) Q.ninvoke req.param ('username')]) .then (funcția (rânduri) 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(); );
Codul este mult mai curat și implică mai puțin rescriere decât abordarea modularizării.
Această soluție este similară cu cea anterioară, dar este mai simplă. Q este un pic cam greu, deoarece implementează ideea întregului promisiune. Biblioteca Step este acolo numai în scopul de a aplatiza iadul callback-ului. Este, de asemenea, un pic mai simplu de utilizat, pentru că pur și simplu apelați singura funcție care este exportată din modul, treci toate apelurile ca parametri și utilizați acest
în locul fiecărui apel invers. Deci, exemplul murdar poate fi transformat în acest lucru, folosind modulul Pas:
(pas) ';' '' '' '' '' '' [req.param ('username')], acest lucru);, funcția checkUsername (eroare, rânduri) if (error) res.writeHead (500); < 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'); ); );
Dezavantajul aici este că nu există un dispozitiv de tratare a erorilor obișnuite. Deși excepțiile aruncate într-o singură invitație de apel sunt transmise la următorul parametru ca primul parametru (deci scriptul nu va coborî din cauza excepției necondiționate), având un singur handler pentru toate erorile este convenabil de cele mai multe ori.
Aceasta este o alegere personală, dar pentru a vă ajuta să alegeți cea potrivită, iată o listă a argumentelor pro și contra fiecărei abordări:
Pro:
Contra:
Pro:
Contra:
Pro:
Contra:
Etapa
funcționează corespunzătorDupă cum puteți vedea, natura asincronă a Node.js poate fi gestionată și iadul de apel invers poate fi evitat. Eu personal folosesc abordarea modularizării, pentru că îmi place să am bine structurat codul meu. Sper că aceste sfaturi vă vor ajuta să vă scrieți codul mai ușor de citit și să vă depanați mai ușor scripturile.