Node.js tillåter dig att skapa appar snabbt och enkelt. Men på grund av sin asynkrona natur kan det vara svårt att skriva läsbar och hanterbar kod. I den här artikeln ska jag visa dig några tips om hur du uppnår det.
Node.js är byggt på ett sätt som tvingar dig att använda asynkrona funktioner. Det betyder callbacks, callbacks och ännu mer callbacks. Du har nog sett eller skrivit en del av kod så här:
app.get ('/ login', funktion (req, res) sql.query ('VÄLJ 1 FRÅN användare WHERE namn =?;', [req.param ('användarnamn')], funktion (fel, rader) om (fel) res.writeHead (500); return res.end (); om (rader.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'); ); ); ); );
Detta är egentligen ett utdrag direkt från en av mina första Node.js-appar. Om du har gjort något mer avancerat i Node.js förstår du förmodligen allt, men problemet här är att koden flyttas till höger varje gång du använder någon asynkron funktion. Det blir svårare att läsa och svårare att felsöka. Lyckligtvis finns det några lösningar för denna röra, så du kan välja rätt för ditt projekt.
Det enklaste tillvägagångssättet är att namnge varje återuppringning (vilket hjälper dig att felsöka koden) och dela upp all kod i moduler. Inloggningsexemplet ovan kan omvandlas till en modul i några enkla steg.
Låt oss börja med en enkel modulstruktur. För att undvika ovanstående situation, när vi bara delar upp röran i mindre messor, låt oss få det att vara en klass:
var util = kräver ('util'); funktion (fel, rader) funktion _checkPassword (fel, rader) funktion _getData (fel, rader) funktion utföra () this.perform = utföra; util.inherits (Logga in, EventEmitter);
Klassen är konstruerad med två parametrar: Användarnamn
och Lösenord
. Titta på provkoden, vi behöver tre funktioner: en för att kontrollera om användarnamnet är korrekt (_checkUsername
), en annan för att kontrollera lösenordet (_checkPassword
) och en till att returnera användarrelaterade data (_hämta data
) och meddela appen att inloggningen lyckades. Det finns även en _checkForErrors
hjälpar som hanterar alla fel. Slutligen finns det en utföra
funktion som startar inloggningsförfarandet (och är den enda offentliga funktionen i klassen). Slutligen ärver vi från EventEmitter
för att förenkla användningen av denna klass.
De _checkForErrors
funktionen kommer att kontrollera om något fel inträffade eller om SQL-frågan inte returnerar några rader och avge det lämpliga felet (med anledningen till att den levererades):
funktion _checkForErrors (fel, rader, anledning) if (error) this.emit ('error', error); återvänd sant; om (rader.längd < 1) this.emit('failure', reason); return true; return false;
Det återkommer också Sann
eller falsk
, beroende på om ett fel inträffade eller inte.
De utföra
funktionen måste endast göra en operation: utföra den första SQL-frågan (för att kontrollera om användarnamnet existerar) och tilldela lämplig återuppringning:
funktion utför () sql.query ('VÄLJ 1 FRÅN användare WHERE namn =?;', [användarnamn], _checkUsername);
Jag antar att du har din SQL-anslutning tillgänglig globalt i sQL
variabel (bara för att förenkla, diskutera om det här är en bra praxis ligger utanför ramen för denna artikel). Och det är det för den här funktionen.
Nästa steg är att kontrollera om användarnamnet är korrekt, och om så är fallet, gör den andra frågan - för att kontrollera lösenordet:
funktion _checkUsername (fel, rader) if (_checkForErrors (fel, rader, 'användarnamn')) return false; else sql.query ('VÄLJ 1 FRÅN användare WHERE namn =? && lösenord = MD5 (?);', [användarnamn, lösenord], _checkPassword);
Ganska mycket samma kod som i det röriga provet, med undantag för felhantering.
Den här funktionen är nästan exakt densamma som den föregående, den enda skillnaden är frågan som heter:
funktion _checkPassword (fel, rader) if (_checkForErrors (fel, rader, lösenord)) return false; else sql.query ("VÄLJ * FRÅN userdata WHERE namn =?;", [användarnamn], _getData);
Den sista funktionen i den här klassen kommer att få data relaterad till användaren (det valfria steget) och avfyra en framgångshändelse med den:
funktion _getData (fel, rader) if (_checkForErrors (fel, rader)) return false; annat this.emit ("success", rader [0]);
Det sista att göra är att exportera klassen. Lägg till den här raden efter alla koden:
module.exports = Logga in;
Detta kommer att göra Logga in
klass det enda som modulen kommer att exportera. Det kan senare användas så här (förutsatt att du har namngett modulfilen login.js
och det finns i samma katalog som huvudskriptet):
var Logga in = kräver ('./ login.js'); ... app.get ('/ login', funktion (req, res) var login = new Login (req.param ('användarnamn'), req.param 'lösenord'); login.on ('fel', funktion (fel) res.writeHead (500); res.end ();); login.on == 'användarnamn') res.end ('Fel användarnamn!'); annars om (orsak == 'lösenord') res.end ('Fel lösenord!';); login.on framgång ", funktion (data) req.session.username = req.param ('användarnamn'); req.session.data = data; res.redirect ('/ userarea');); login.perform (); );
Här är några fler rader kod, men läsbarheten av koden har ökat, ganska märkbart. Denna lösning använder inte heller några externa bibliotek, vilket gör det perfekt om någon ny kommer till ditt projekt.
Det var det första tillvägagångssättet, låt oss fortsätta till den andra.
Att använda löften är ett annat sätt att lösa detta problem. Ett löfte (som du kan läsa i länken som tillhandahålls) "representerar det slutliga värdet som returneras från den ena slutförandet av en operation". I praktiken betyder det att du kan köra samtalen för att platta pyramiden och göra koden lättare att läsa.
Vi använder Q-modulen, tillgänglig i NPM-förvaret.
Innan vi börjar, låt mig introducera dig till Q. För statiska klasser (moduler) ska vi främst använda Q.nfcall
fungera. Det hjälper oss att konvertera alla funktioner som följer Node.js återkopplingsmönster (där parametrarna för återuppringningen är felet och resultatet) till ett löfte. Den används så här:
Q.nfcall (http.get, alternativ);
Det är ganska likt Object.prototype.call
. Du kan också använda Q.nfapply
som liknar Object.prototype.apply
:
Q.nfapply (fs.readFile, ['filename.txt', 'utf-8']);
När vi också skapar löftet lägger vi till varje steg med därefter (stepCallback)
metod, fånga fel med spärren (errorCallback)
och avsluta med Gjort()
.
I detta fall, sedan sQL
objekt är en förekomst, inte en statisk klass, vi måste använda Q.ninvoke
eller Q.npost
, som liknar ovanstående. Skillnaden är att vi passerar metodens namn som en sträng i det första argumentet och förekomsten av den klass som vi vill arbeta med som en andra för att undvika att metoden är unbinded från förekomsten.
Det första du behöver göra är att genomföra det första steget, med hjälp av Q.nfcall
eller Q.nfapply
(använd den som du gillar mer, det finns ingen skillnad nedan):
var Q = kräver ('q'); ... app.get ('/ login', funktion (req, res) Q.ninvoke ('query', sql, 'VÄLJ 1 FRÅN användare WHERE namn =?;', [ req.param ('användarnamn')]));
Observera bristen på en semikolon i slutet av linjen - funktionssamtalen kommer att vara kedjda så att den inte kan vara där. Vi ringer bara på sql.query
som i det röriga exemplet, men vi släpper bort återuppringningsparametern - det hanteras av löftet.
Nu kan vi skapa återuppringningen för SQL-frågan, den kommer nästan att vara identisk med den som finns i "pyramiden av döm" -exempel. Lägg till detta efter Q.ninvoke
ring upp:
.då (funktion (rader) if (rader.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') ]); )
Som ni kan se lägger vi tillbaka återuppringningen (nästa steg) med hjälp av sedan
metod. Också i återuppringningen lämnar vi bort fel
parameter, eftersom vi kommer att fånga alla fel senare. Vi kontrollerar manuellt om frågan returnerade någonting, och om så är fallet återvänder vi nästa löfte som ska utföras (igen, ingen semikolon på grund av kedjan).
Precis som med modulariseringsexemplet är kontrollen av lösenordet nästan identiskt med att kontrollera användarnamnet. Detta borde gå strax efter det sista sedan
ring upp:
.då (funktion (rader) if (rader.length < 1) res.end('Wrong password!'); else return Q.ninvoke('query', sql, 'SELECT * FROM userdata WHERE name = ?;', [ req.param('username') ]); )
Det sista steget kommer att vara det där vi lägger användarnas data i sessionen. Återigen är återuppringningen inte mycket annorlunda än det röra sig i exemplet:
.sedan (funktion (rader) req.session.username = req.param ('användarnamn'); req.session.data = rader [0]; res.rediect ('/ userarea');)
När du använder löften och Q-biblioteket hanteras alla fel av återuppringningssatsen med hjälp av fånga
metod. Här skickar vi bara HTTP 500, oavsett vad felet är, som i exemplen ovan:
.fånga (funktion (fel) res.writeHead (500); res.end ();) .done ();
Därefter måste vi ringa Gjort
metod för att "se till att om ett fel inte hanteras före slutet, kommer det att bli omhämtat och rapporterat" (från bibliotekets README). Nu ska vår vackert utplattade kod se ut så här (och beter sig som den röriga):
var Q = kräver ('q'); ... app.get ('/ login', funktion (req, res) Q.ninvoke ('query', sql, 'VÄLJ 1 FRÅN användare WHERE namn =?;', [ req.param ('användarnamn')]) .then (funktion (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 är mycket renare, och det innebar mindre omskrivning än modulariseringsmetoden.
Denna lösning liknar den tidigare, men det är enklare. Q är lite tung, eftersom det genomför hela löftetidén. Stegsbiblioteket finns bara för att fläta återkallnings helvetet. Det är också lite enklare att använda, eftersom du bara kallar den enda funktionen som exporteras från modulen, skicka alla dina återuppringningar som parametrar och använd detta
i stället för varje återkallelse. Så det rotiga exemplet kan omvandlas till detta med hjälp av steg-modulen:
varsteg = kräver ('steg'); ... app.get ('/ login', funktion (req, res) steg (funktionstart () sql.query ('VÄLJ 1 FRÅN användare WHERE name =?;' [req.param ('användarnamn')], det här);, funktionskontrolls användarnamn (fel, rader) if (error) res.writeHead (500); return res.end (); om (rader.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'); ); );
Nackdelen här är att det inte finns någon vanlig felhanterare. Även om eventuella undantag som kastas i en återuppringning vidarebefordras till nästa som den första parametern (så manuset kommer inte att gå ner på grund av det oavsiktliga undantaget), att ha en hanterare för alla fel är bekväm mesteparten av tiden.
Det är ganska mycket ett personligt val, men för att hjälpa dig att välja rätt, här är en lista över fördelar och nackdelar med varje tillvägagångssätt:
Fördelar:
Nackdelar:
Fördelar:
Nackdelar:
Fördelar:
Nackdelar:
steg
fungera ordentligtSom du kan se, kan Node.js asynkrona natur hanteras och återkallnings helvetet kan undvikas. Jag använder personligen modulariseringsmetoden, eftersom jag tycker om att min kod är välstrukturerad. Jag hoppas att dessa tips hjälper dig att skriva din kod mer läsbar och felsöka dina skript enklare.