Gestionarea naturii asincrone a Node.js

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.


Întoarcere înapoi sau Piramida Doomului

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..


Soluția 1: denumirea și modularizarea apelurilor

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.

Structura

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.

Ajutorul

_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.

Efectuarea conectării

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.

Verificarea numelui de utilizator

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.

Verificarea parolei

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); 

Obținerea datelor legate de utilizator

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]); 

Sfaturi și utilizări finale

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.


Soluția 2: Promisiunile

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.

Q pe scurt

Î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ță.

Pregătirea promisiunii

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.

Verificarea numelui de utilizator

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).

Verificarea parolei

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') ]);  )

Obținerea datelor legate de utilizator

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');)

Verificarea erorilor

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.


Soluția 3: Biblioteca pas

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.


Care dintre ele să alegeți?

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:

Modularizarea:

Pro:

  • Nu există biblioteci externe
  • Ajută la îmbunătățirea reutilizării codului

Contra:

  • Mai multe coduri
  • Multe rescrieri dacă convertiți un proiect existent

Promisiunile (Q):

Pro:

  • Mai puțin cod
  • Doar puțină rescriere dacă este aplicată unui proiect existent

Contra:

  • Trebuie să folosiți o bibliotecă externă
  • Necesită un pic de învățare

Biblioteca de etape:

Pro:

  • Ușor de utilizat, fără învățare
  • Destul de mult copiați și lipiți dacă convertiți un proiect existent

Contra:

  • Nu există un dispozitiv de tratare a erorilor
  • Un pic mai greu pentru a linia asta Etapa funcționează corespunzător

Concluzie

După 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.

Cod