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.
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.
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.
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.
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.
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.
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.
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);
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]);
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.
Å 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.
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.
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.
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).
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') ]); )
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');)
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.
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..
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:
Pros:
Ulemper:
Pros:
Ulemper:
Pros:
Ulemper:
skritt
fungere ordentligSom 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.