Met Node.js kun je snel en eenvoudig apps maken. Maar vanwege zijn asynchrone aard is het misschien moeilijk om leesbare en beheersbare code te schrijven. In dit artikel laat ik je een paar tips zien over hoe je dat kunt bereiken.
Node.js is zo gebouwd dat je gedwongen wordt om asynchrone functies te gebruiken. Dat betekent callbacks, callbacks en zelfs meer callbacks. Je hebt waarschijnlijk zelf stukjes code gezien of geschreven:
app.get ('/ login', functie (req, res) sql.query ('SELECT 1 FROM users WHERE name =?;', [req.param ('username')], function (error, rows) if (error) res.writeHead (500); return res.end (); if (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'); ); ); ); );
Dit is eigenlijk een fragment uit een van mijn eerste Node.js-apps. Als je iets geavanceerder hebt gedaan in Node.js, begrijp je waarschijnlijk alles, maar het probleem hier is dat de code naar rechts gaat telkens wanneer je een asynchrone functie gebruikt. Het wordt moeilijker om te lezen en moeilijker te debuggen. Gelukkig zijn er een paar oplossingen voor deze puinhoop, dus je kunt de juiste kiezen voor je project.
De eenvoudigste benadering is om elke callback (die u zal helpen bij het debuggen van de code) een naam te geven en al uw code in modules te splitsen. Het login-voorbeeld hierboven kan in een paar eenvoudige stappen in een module worden omgezet.
Laten we beginnen met een eenvoudige modulestructuur. Om de bovenstaande situatie te voorkomen, wanneer je de puinhoop in kleinere puinhoop splitst, is het een klasse:
var util = require ('util'); function Login (gebruikersnaam, wachtwoord) functie _checkForErrors (error, rows, reason) functie _checkUsername (error, rows) function _checkPassword (error, rows) function _getData (error, rows) function perform () this.perform = uitvoeren; util.inherits (Login, EventEmitter);
De klasse is opgebouwd met twee parameters: gebruikersnaam
en wachtwoord
. Als we naar de voorbeeldcode kijken, hebben we drie functies nodig: één om te controleren of de gebruikersnaam correct is (_checkUsername
), een andere om het wachtwoord te controleren (_checkPassword
) en nog een om de gebruikersgerelateerde gegevens te retourneren (_gegevens verkrijgen
) en meldt de app dat de aanmelding succesvol was. Er is ook een _checkForErrors
helper, die alle fouten zal afhandelen. Eindelijk is er een uitvoeren
functie, waarmee de inlogprocedure wordt gestart (en is de enige openbare functie in de klas). Ten slotte erven we van EventEmitter
om het gebruik van deze klasse te vereenvoudigen.
De _checkForErrors
functie controleert of er een fout is opgetreden of als de SQL-query geen rijen retourneert en de juiste fout uitzendt (met de reden die is opgegeven):
function _checkForErrors (error, rows, reason) if (error) this.emit ('error', error); geef waar terug; if (rows.length < 1) this.emit('failure', reason); return true; return false;
Het keert ook terug waar
of vals
, afhankelijk van of er een fout is opgetreden of niet.
De uitvoeren
functie hoeft maar één handeling uit te voeren: voer de eerste SQL-query uit (om te controleren of de gebruikersnaam bestaat) en wijs de juiste callback toe:
function perform () sql.query ('SELECT 1 FROM users WHERE name =?;', [username], _checkUsername);
Ik neem aan dat je je SQL-verbinding globaal toegankelijk hebt in de sql
variabele (alleen om te vereenvoudigen, bespreken of dit een goede praktijk is, valt buiten het bestek van dit artikel). En dat is het voor deze functie.
De volgende stap is om te controleren of de gebruikersnaam correct is en, als dat het geval is, de tweede zoekopdracht in te stellen - om het wachtwoord te controleren:
function _checkUsername (error, rows) if (_checkForErrors (error, rows, 'username')) return false; else sql.query ('SELECT 1 FROM users WHERE name =? && password = MD5 (?);', [gebruikersnaam, wachtwoord], _checkPassword);
Vrijwel dezelfde code als in het rommelige voorbeeld, met uitzondering van foutafhandeling.
Deze functie is bijna precies hetzelfde als de vorige, het enige verschil is de query met de naam:
function _checkPassword (error, rows) if (_checkForErrors (error, rows, 'password')) return false; else sql.query ('SELECT * FROM userdata WHERE name =?;', [username], _getData);
De laatste functie in deze klasse krijgt de gegevens gerelateerd aan de gebruiker (de optionele stap) en activeert hiermee een succesgebeurtenis:
functie _getData (error, rows) if (_checkForErrors (error, rows)) return false; else this.emit ('success', rijen [0]);
Het laatste wat je moet doen is de klasse exporteren. Voeg deze regel toe na alle code:
module.exports = Inloggen;
Dit zal het maken Log in
klasse het enige dat de module zal exporteren. Het kan later op deze manier worden gebruikt (ervan uitgaande dat u het modulebestand een naam hebt gegeven login.js
en het bevindt zich in dezelfde map als het hoofdscript):
var Login = require ('./ login.js'); ... app.get ('/ login', functie (req, res) var login = new Login (req.param ('gebruikersnaam'), req.param ( 'wachtwoord)); login.on (' error ', function (error) res.writeHead (500); res.end ();); login.on (' failure ', function (reason) if (reason == 'gebruikersnaam') res.end ('Verkeerde gebruikersnaam!'); else if (reason == 'password') res.end ('Wrong password!';;); login.on (' succes ', functie (gegevens) req.session.username = req.param (' gebruikersnaam '); req.session.data = data; res.redirect (' / userarea ');); login.perform (); );
Hier zijn nog een paar coderegels, maar de leesbaarheid van de code is behoorlijk toegenomen. Ook gebruikt deze oplossing geen externe bibliotheken, wat het perfect maakt als iemand nieuw bij je project komt.
Dat was de eerste benadering, laten we naar de tweede gaan.
Het gebruik van beloften is een andere manier om dit probleem op te lossen. Een belofte (zoals u kunt lezen in de bijgevoegde link) "vertegenwoordigt de uiteindelijke waarde die is teruggegeven na het voltooien van een bewerking". In de praktijk betekent dit dat u de oproepen kunt ketenen om de piramide af te vlakken en de code leesbaarder te maken.
We zullen de Q-module gebruiken, beschikbaar in de NPM-repository.
Voordat we beginnen, laat ik je voorstellen aan de Q. Voor statische klassen (modules) zullen we voornamelijk de Q.nfcall
functie. Het helpt ons bij de conversie van elke functie volgens het callback-patroon van Node.js (waarbij de parameters van de callback de fout en het resultaat zijn) naar een belofte. Het wordt als volgt gebruikt:
Q.nfcall (http.get, opties);
Het lijkt op veel Object.prototype.call
. U kunt ook de Q.nfapply
wat lijkt op Object.prototype.apply
:
Q.nfapply (fs.readFile, ['filename.txt', 'utf-8']);
Ook, wanneer we de belofte creëren, voegen we elke stap toe met de vervolgens (stepCallback)
methode, vang de fouten op met vangst (errorCallback)
en eindigen met gedaan()
.
In dit geval, sinds de sql
object is een instantie, geen statische klasse, we moeten gebruiken Q.ninvoke
of Q.npost
, die vergelijkbaar zijn met het bovenstaande. Het verschil is dat we de naam van de methode doorgeven als een tekenreeks in het eerste argument, en de instantie van de klasse waarmee we willen werken als een tweede, om te voorkomen dat de methode unbinded van de instantie.
Het eerste dat u moet doen, is om de eerste stap uit te voeren, met behulp van Q.nfcall
of Q.nfapply
(gebruik degene die je leuker vindt, er is geen verschil onder):
var Q = require ('q'); ... app.get ('/ login', functie (req, res) Q.ninvoke ('query', sql, 'SELECT 1 FROM users WHERE name =?;', [ req.param ('gebruikersnaam')]));
Let op het ontbreken van een puntkomma aan het einde van de regel - de functie-oproepen worden geketend, zodat het er niet kan zijn. We bellen gewoon het sql.query
zoals in het rommelige voorbeeld, maar we laten de callback-parameter weg - het wordt afgehandeld door de belofte.
Nu kunnen we de callback voor de SQL-query maken, deze zal bijna identiek zijn aan die in het voorbeeld van de "Pyramide van de ondergang". Voeg dit toe na de Q.ninvoke
bellen:
.dan (functie (rijen) if (rijen.lengte < 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') ]); )
Zoals je kunt zien verbinden we de callback (de volgende stap) met de dan
methode. Ook in de callback laten we de fout
parameter, omdat we alle fouten later zullen opvangen. We controleren handmatig of de query iets heeft geretourneerd en zo ja, dan geven we de volgende toe te voegen belofte om te worden uitgevoerd (nogmaals, geen puntkomma vanwege de keten).
Net als bij het modularisatievoorbeeld is het controleren van het wachtwoord bijna identiek aan het controleren van de gebruikersnaam. Dit zou direct na de laatste moeten gaan dan
bellen:
.dan (functie (rijen) if (rijen.lengte < 1) res.end('Wrong password!'); else return Q.ninvoke('query', sql, 'SELECT * FROM userdata WHERE name = ?;', [ req.param('username') ]); )
De laatste stap is die waarbij we de gegevens van de gebruikers in de sessie plaatsen. Nogmaals, de callback verschilt niet veel van het rommelige voorbeeld:
.then (functie (rijen) req.session.username = req.param ('gebruikersnaam'); req.session.data = rijen [0]; res.rediect ('/ userarea');)
Bij het gebruik van beloftes en de Q-bibliotheek worden alle fouten afgehandeld door de terugbelverzameling met behulp van de vangst
methode. Hier sturen we alleen de HTTP 500, ongeacht de fout, zoals in de bovenstaande voorbeelden:
.catch (functie (fout) res.writeHead (500); res.end ();) .done ();
Daarna moeten we de gedaan
methode om "ervoor te zorgen dat, als een fout niet voor het einde wordt afgehandeld, deze opnieuw wordt weergegeven en wordt gerapporteerd" (uit de README van de bibliotheek). Nu zou onze mooi afgeplatte code er zo uit moeten zien (en zich gedragen als de rommelige code):
var Q = require ('q'); ... app.get ('/ login', functie (req, res) Q.ninvoke ('query', sql, 'SELECT 1 FROM users WHERE name =?;', [ req.param ('gebruikersnaam')]) .then (functie (rijen) if (rijen.lengte < 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(); );
De code is veel schoner, en er was minder herschrijving nodig dan bij de modularisatie.
Deze oplossing lijkt op de vorige, maar is eenvoudiger. Q is een beetje zwaar, omdat het het hele beloofde idee implementeert. De Step-bibliotheek is er alleen met het doel om de callback-hel af te vlakken. Het is ook een beetje eenvoudiger te gebruiken, omdat je gewoon de enige functie aanroept die wordt geëxporteerd vanuit de module, al je callbacks doorgeeft als parameters en gebruik deze
in plaats van elke callback. Het slordige voorbeeld kan dus worden omgezet in dit, met behulp van de Stap-module:
var step = require ('step'); ... app.get ('/ login', functie (req, res) step (function start () sql.query ('SELECT 1 FROM users WHERE name =?;', [req.param ('gebruikersnaam')], this);, functie checkUsername (error, rows) if (error) res.writeHead (500); return res.end (); if (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'); ); );
Het nadeel hier is dat er geen veel voorkomende foutafhandelaar is. Hoewel alle uitzonderingen die in één callback worden gegooid, worden doorgegeven aan de volgende parameter (dus het script zal niet worden afgebroken vanwege de niet-afgevangen uitzondering), is het meestal handig om één handler voor alle fouten te hebben.
Dat is zo'n beetje een persoonlijke keuze, maar om u te helpen de juiste te kiezen, volgt hier een lijst met voor- en nadelen van elke aanpak:
Voors:
nadelen:
Voors:
nadelen:
Voors:
nadelen:
stap
goed functionerenZoals u ziet, kan de asynchrone aard van Node.js worden beheerd en kan de callback-hel worden voorkomen. Ik gebruik persoonlijk de modularisatiebenadering, omdat ik mijn code graag goed gestructureerd wil hebben. Ik hoop dat deze tips je helpen om je code leesbaarder te maken en je scripts gemakkelijker te debuggen.