Administrere den asynkrone naturen til Node.js

Node.js lar deg lage apper raskt og enkelt. Men på grunn av sin asynkrone natur kan det være vanskelig å skrive lesbar og håndterbar kode. I denne artikkelen vil jeg vise deg noen tips om hvordan du oppnår det.


Tilbakekall Helvete eller Doompyramiden

Node.js er bygget på en måte som tvinger deg til å bruke asynkrone funksjoner. Det betyr tilbakekallinger, tilbakeringinger og enda flere tilbakeringinger. Du har sikkert sett eller skrevet selv kodenummer som denne:

app.get ('/ login', funksjon (req, res) sql.query ('VELG 1 FRA brukere WHERE navn =?;', [req.param ('brukernavn')], funksjon (feil, rader)  hvis (feil) res.writeHead (500); return res.end (); hvis (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'); );  );  ); );

Dette er faktisk en utdrag direkte fra en av mine første Node.js-apper. Hvis du har gjort noe mer avansert i Node.js forstår du sannsynligvis alt, men problemet er at koden beveger seg til høyre hver gang du bruker en asynkron funksjon. Det blir vanskeligere å lese og vanskeligere å feilsøke. Heldigvis finnes det noen få løsninger på dette rotet, slik at du kan velge den rette for prosjektet ditt.


Løsning 1: Tilbakeringing Navngivning og Modularisering

Den enkleste tilnærmingen ville være å nevne alle tilbakeringinger (som vil hjelpe deg med å feilsøke koden) og dele all koden i moduler. Påloggingseksemplet ovenfor kan bli omgjort til en modul i noen enkle trinn.

Strukturen

La oss starte med en enkel modulstruktur. For å unngå situasjonen ovenfor, når du bare deler rotet i mindre messer, la oss få det til å være en klasse:

var util = krever ('util'); funksjon _checkForErrors (feil, rader, grunn)  funksjon _checkUsername (feil, rader)  funksjon _checkPassword (feil, rader)  funksjon _getData (feil, rader)  funksjon utføre  this.perform = utføre;  util.inherits (Login, EventEmitter);

Klassen er konstruert med to parametere: brukernavn og passord. Når vi ser på prøvekoden, trenger vi tre funksjoner: en for å sjekke om brukernavnet er riktig (_checkUsername), en annen for å sjekke passordet (_checkPassword) og en til å returnere brukerrelaterte data (_getData) og varsle appen om at påloggingen var vellykket. Det er også en _checkForErrors hjelper, som håndterer alle feil. Til slutt er det a utføre funksjon, som vil starte påloggingsprosedyren (og er den eneste offentlige funksjonen i klassen). Til slutt arver vi fra EventEmitter for å forenkle bruken av denne klassen.

Hjelperen

De _checkForErrors funksjonen vil sjekke om det oppsto en feil eller hvis SQL-spørringen ikke returnerer noen rader, og avgir den aktuelle feilen (med den grunn som ble levert):

funksjon _checkForErrors (feil, rader, grunn) if (error) this.emit ('error', error); returnere sant;  hvis (rader.lengde < 1)  this.emit('failure', reason); return true;  return false; 

Den kommer også tilbake ekte eller falsk, avhengig av om det oppsto en feil eller ikke.

Utfører påloggingen

De utføre funksjonen må bare utføre en operasjon: Utfør den første SQL-spørringen (for å sjekke om brukernavnet eksisterer) og tilordne riktig tilbakeringing:

funksjon utføre () sql.query ('VELG 1 fra brukere WHERE navn =?;', [brukernavn], _checkUsername); 

Jeg antar at du har din SQL-tilkobling tilgjengelig globalt i sql variabel (bare for å forenkle, diskutere om dette er en god praksis er utenfor rammen av denne artikkelen). Og det er det for denne funksjonen.

Kontrollerer brukernavnet

Det neste trinnet er å sjekke om brukernavnet er riktig, og hvis så brann det andre spørsmålet - for å sjekke passordet:

funksjon _checkUsername (feil, rader) if (_checkForErrors (feil, rader, 'brukernavn')) return false;  else sql.query ('VELG 1 FRA brukere WHERE navn =? && passord = MD5 (?);', [brukernavn, passord], _checkPassword); 

Ganske mye den samme koden som i det rotete eksemplet, med unntak av feilhåndtering.

Kontrollerer passordet

Denne funksjonen er nesten nøyaktig den samme som den forrige, den eneste forskjellen er spørringen som heter:

funksjon _checkPassword (feil, rader) if (_checkForErrors (feil, rader, 'passord')) return false;  else sql.query ('SELECT * FROM userdata hvor navnet =?;', [brukernavn], _getData); 

Få brukerrelaterte data

Den siste funksjonen i denne klassen vil få data relatert til brukeren (valgfritt trinn) og brann en suksesshendelse med den:

funksjon _getData (feil, rader) if (_checkForErrors (feil, rader)) return false;  ellers this.emit ('suksess', rader [0]); 

Endelig toll og bruk

Den siste tingen å gjøre er å eksportere klassen. Legg til denne linjen etter all kode:

module.exports = Logg inn;

Dette vil gjøre Logg Inn klasse det eneste som modulen vil eksportere. Den kan senere brukes som dette (forutsatt at du har navngitt modulfilen login.js og det er i samme katalog som hovedskriptet):

var Logg inn = krever ('./ login.js'); ... app.get ('/ logg inn', funksjon (req, res) var login = new Logg inn (req.param ('brukernavn'), req.param 'passord'); login.on ('feil', funksjon (feil) res.writeHead (500); res.end ();); login.on ('feil' == 'brukernavn') res.end ('Feil brukernavn!'); else if (reason == 'passord') res.end ('Feil passord!');); login.on suksess ', funksjon (data) req.session.username = req.param (' brukernavn '); req.session.data = data; res.redirect (' / userarea ');); login.perform (); );

Her er noen flere linjer med kode, men lesbarheten av koden har økt, ganske merkbart. Denne løsningen bruker ikke noen eksterne biblioteker, noe som gjør det perfekt hvis noen nye kommer til prosjektet ditt.

Det var den første tilnærmingen, la oss fortsette til den andre.


Løsning 2: Løfter

Å bruke løfter er en annen måte å løse dette problemet på. Et løfte (som du kan lese i lenken som er oppgitt) "representerer den endelige verdien returnert fra den endelige gjennomføringen av en operasjon". I praksis betyr det at du kan kjede anropene for å flate pyramiden og gjøre koden enklere å lese.

Vi vil bruke Q-modulen, tilgjengelig i NPM-depotet.

Q i nøtteskallet

Før vi begynner, la meg introdusere deg til Q. For statiske klasser (moduler) vil vi primært bruke Q.nfcall funksjon. Det hjelper oss med å konvertere hver funksjon som følger Node.js tilbakeringingsmønster (der parametrene for tilbakeringingen er feilen og resultatet) til et løfte. Det brukes slik:

Q.nfcall (http.get, alternativer);

Det er ganske mye som Object.prototype.call. Du kan også bruke Q.nfapply som ligner på Object.prototype.apply:

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

Når vi lager løftet, legger vi også til hvert trinn med deretter (stepCallback) metode, ta feilene med catch (errorCallback) og avslutt med gjort ().

I dette tilfellet, siden sql objekt er en forekomst, ikke en statisk klasse, vi må bruke Q.ninvoke eller Q.npost, som ligner på ovennevnte. Forskjellen er at vi passerer metodens navn som en streng i det første argumentet, og forekomsten av klassen som vi vil jobbe med som en annen, for å unngå at metoden er unbinded fra forekomsten.

Forbereder løftet

Det første du må gjøre er å utføre det første trinnet ved å bruke Q.nfcall eller Q.nfapply (bruk den du liker mer, det er ingen forskjell under):

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

Legg merke til mangelen på et semikolon på slutten av linjen - funksjonskallene vil bli koblet slik at det ikke kan være der. Vi ringer bare på sql.query som i det rote eksemplet, men vi utelater tilbakeringingsparameteren - det håndteres av løftet.

Kontrollerer brukernavnet

Nå kan vi lage tilbakekallingen til SQL-spørringen, den vil nesten være identisk med den i "pyramiden av dømmekraft" -eksempel. Legg til dette etter Q.ninvoke anrop:

.da (funksjon (rader) hvis (rader.lengde < 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') ]);  )

Som du kan se er vi vedlagt tilbakeringingen (neste trinn) ved hjelp av deretter metode. Også i tilbakekallingen slipper vi ut feil parameter, fordi vi vil fange alle feilene senere. Vi kontrollerer manuelt, hvis spørringen returnerte noe, og hvis så returnerer vi det neste løftet som skal utføres (igjen, ikke semikolon på grunn av kjetting).

Kontrollerer passordet

Som med modulasjonseksemplet, er sjekken passordet nesten identisk med å sjekke brukernavnet. Dette bør gå like etter sist deretter anrop:

.da (funksjon (rader) hvis (rader.lengde < 1)  res.end('Wrong password!');  else  return Q.ninvoke('query', sql, 'SELECT * FROM userdata WHERE name = ?;', [ req.param('username') ]);  )

Få brukerrelaterte data

Det siste trinnet er det vi legger brukerens data i økten. Nok en gang er tilbakeringingen ikke mye forskjellig fra det rotete eksemplet:

.da (funksjon (rader) req.session.username = req.param ('brukernavn'); req.session.data = rader [0]; res.rediect ('/ userarea');)

Kontrollerer feil

Når du bruker løfter og Q-biblioteket, håndteres alle feilene av tilbakeringingssettet ved hjelp av å fange metode. Her sender vi bare HTTP 500, uansett hva feilen er, som i eksemplene ovenfor:

.fangst (funksjon (feil) res.writeHead (500); res.end ();) .done ();

Etter det må vi ringe ferdig metode for å "sørge for at hvis en feil ikke håndteres før slutten, vil den bli retrodusert og rapportert" (fra bibliotekets README). Nå må vår vakkert flatete kode se slik ut (og oppføre seg akkurat som den rotete):

var Q = krever ('q'); ... app.get ('/ login', funksjon (req, res) Q.ninvoke ('query', sql, 'SELECT 1 FROM users WHERE name =?;' req.param ('brukernavn')]) .then (funksjon (rader) 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(); );

Koden er mye renere, og det innebar mindre omskrivning enn modulariseringsmetoden.


Løsning 3: Trinnbibliotek

Denne løsningen ligner på den forrige, men det er enklere. Q er litt tung, fordi den implementerer hele løftetidene. Steg-biblioteket er der bare for å flette tilbakekallingshelvet. Det er også litt enklere å bruke, fordi du bare kaller den eneste funksjonen som eksporteres fra modulen, passere alle tilbakeringingene dine som parametere og bruk dette i stedet for hver tilbakeringing. Så det rotete eksemplet kan konverteres til dette ved hjelp av trinnmodulen:

var trinn = krever ('trinn'); ... app.get ('/ login', funksjon (req, res) trinn (funksjonstart () sql.query ('VELG 1 fra brukere hvor navnet =?;' [req.param ('brukernavn')], dette);, funksjonskontrollnavn (feil, rader) hvis (feil) res.writeHead (500); return res.end (); hvis (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');  ); );

Ulempen her er at det ikke er noen vanlig feilhåndterer. Selv om noen unntak kastet i en tilbakeringing, blir sendt til neste som den første parameteren (slik at manuset ikke vil gå ned på grunn av det uncaught unntaket), er det mest praktisk å ha en håndterer for alle feilene..


Hvilken å velge?

Det er ganske mye et personlig valg, men for å hjelpe deg med å velge den rette, er det en liste over fordeler og ulemper ved hver tilnærming:

modularisering:

Pros:

  • Ingen eksterne biblioteker
  • Hjelper med å gjøre koden mer gjenbrukbar

Ulemper:

  • Mer kode
  • Mye omskriving hvis du konverterer et eksisterende prosjekt

Løfter (Q):

Pros:

  • Mindre kode
  • Bare litt omskrivning hvis brukt på et eksisterende prosjekt

Ulemper:

  • Du må bruke et eksternt bibliotek
  • Krever litt læring

Trinnbibliotek:

Pros:

  • Enkel å bruke, ingen læring kreves
  • Ganske mye kopi og lim inn om du konverterer et eksisterende prosjekt

Ulemper:

  • Ingen vanlig feilhåndterer
  • Litt vanskeligere å innrømme det skritt fungere ordentlig

Konklusjon

Som du kan se, kan Node.js asynkrone karakter styres, og tilbakekallingshelvet kan unngås. Jeg bruker personlig modularisering, fordi jeg liker å ha min kode godt strukturert. Jeg håper disse tipsene vil hjelpe deg med å skrive koden din mer lesbar og feilsøke skriptene dine lettere.