Mit Node.js können Sie Apps schnell und einfach erstellen. Aufgrund seines asynchronen Charakters kann es jedoch schwierig sein, lesbaren und verwaltbaren Code zu schreiben. In diesem Artikel zeige ich Ihnen ein paar Tipps, wie Sie dies erreichen können.
Node.js ist so aufgebaut, dass Sie die Verwendung asynchroner Funktionen erzwingen. Das bedeutet Callbacks, Callbacks und noch mehr Callbacks. Sie haben wahrscheinlich Codeelemente wie diese selbst gesehen oder geschrieben:
app.get ('/ login', Funktion (req, res) sql.query ('SELECT 1 FROM Benutzer WHERE name =?;', [req.param ('Benutzername')]), Funktion (Fehler, Zeilen) if (Fehler) 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'); ); ); ); );
Dies ist eigentlich ein Ausschnitt aus einer meiner ersten Node.js-Apps. Wenn Sie in Node.js etwas Fortgeschrittenes getan haben, verstehen Sie wahrscheinlich alles, aber das Problem ist, dass der Code bei jeder asynchronen Funktion nach rechts verschoben wird. Es wird schwieriger zu lesen und schwieriger zu debuggen. Glücklicherweise gibt es einige Lösungen für dieses Durcheinander, sodass Sie die richtige für Ihr Projekt auswählen können.
Am einfachsten wäre es, jeden Callback zu benennen (was Ihnen beim Debuggen des Codes helfen wird) und den gesamten Code in Module aufzuteilen. Das oben beschriebene Anmeldebeispiel kann in wenigen einfachen Schritten in ein Modul umgewandelt werden.
Beginnen wir mit einer einfachen Modulstruktur. Um die obige Situation zu vermeiden, wenn Sie das Chaos in kleinere Chaos aufteilen, lassen Sie uns eine Klasse sein:
var util = erfordern ('util'); Funktion Login (Benutzername, Passwort) Funktion _checkForErrors (Fehler, Zeilen, Grund) Funktion _checkUsername (Fehler, Zeilen) Funktion _checkPassword (Fehler, Zeilen) Funktion _getData (Fehler, Zeilen) Funktion perform () this.perform = perform; util.inherits (Login, EventEmitter);
Die Klasse besteht aus zwei Parametern: Nutzername
und Passwort
. Wenn wir uns den Beispielcode ansehen, brauchen wir drei Funktionen: eine, um zu überprüfen, ob der Benutzername korrekt ist (_checkUsername
), ein weiteres zur Überprüfung des Passworts (_checkPassword
) und eine weitere, um die benutzerbezogenen Daten zurückzugeben (_Daten empfangen
) und benachrichtigen Sie die App, dass die Anmeldung erfolgreich war. Da ist auch ein _checkForErrors
Helfer, der alle Fehler behandelt. Schließlich gibt es eine ausführen
Funktion, die die Login-Prozedur startet (und die einzige öffentliche Funktion in der Klasse ist). Schließlich erben wir von EventEmitter
um die Verwendung dieser Klasse zu vereinfachen.
Das _checkForErrors
Die Funktion überprüft, ob ein Fehler aufgetreten ist oder ob die SQL-Abfrage keine Zeilen zurückgibt, und gibt den entsprechenden Fehler aus (mit dem angegebenen Grund):
Funktion _checkForErrors (Fehler, Zeilen, Grund) if (error) this.emit ('error', error); wahr zurückgeben; if (Zeilen.Länge < 1) this.emit('failure', reason); return true; return false;
Es kehrt auch zurück wahr
oder falsch
, abhängig davon, ob ein Fehler aufgetreten ist oder nicht.
Das ausführen
Die Funktion muss nur eine Operation ausführen: Führen Sie die erste SQL-Abfrage aus (um zu überprüfen, ob der Benutzername vorhanden ist) und weisen Sie den entsprechenden Rückruf zu:
Funktion perform () sql.query ('SELECT 1 FROM Benutzer WHERE name =?;', [Benutzername], _checkUsername);
Ich gehe davon aus, dass Sie Ihre SQL-Verbindung global im Internet haben sql
Variable (nur um zu vereinfachen, zu diskutieren, ob dies eine gute Praxis ist, würde den Rahmen dieses Artikels sprengen). Und das war es für diese Funktion.
Der nächste Schritt besteht darin, zu prüfen, ob der Benutzername korrekt ist, und wenn ja, die zweite Abfrage auszulösen, um das Kennwort zu prüfen:
Funktion _checkUsername (Fehler, Zeilen) if (_checkForErrors (Fehler, Zeilen, 'Benutzername')) Rückgabe falsch; else sql.query ('SELECT 1 FROM Benutzer WHERE name =? && password = MD5 (?);', [Benutzername, Passwort], _checkPassword);
Ziemlich derselbe Code wie im chaotischen Beispiel, mit Ausnahme der Fehlerbehandlung.
Diese Funktion ist fast genau dieselbe wie die vorige, der einzige Unterschied besteht in der aufgerufenen Abfrage:
Funktion _checkPassword (Fehler, Zeilen) if (_checkForErrors (Fehler, Zeilen, 'Kennwort')) Rückgabe falsch; else sql.query ('SELECT * FROM Benutzerdaten WHERE name =?;', [Benutzername], _getData);
Die letzte Funktion in dieser Klasse erhält die auf den Benutzer bezogenen Daten (den optionalen Schritt) und löst damit ein Erfolgsereignis aus:
function _getData (Fehler, Zeilen) if (_checkForErrors (Fehler, Zeilen)) Rückgabe falsch; else this.emit ('success', Zeilen [0]);
Als letztes müssen Sie die Klasse exportieren. Fügen Sie diese Zeile nach dem gesamten Code ein:
module.exports = Anmelden;
Das wird das machen Anmeldung
Klasse das einzige, was das Modul exportieren wird. Sie kann später so verwendet werden (vorausgesetzt, Sie haben die Moduldatei benannt.) login.js
und es befindet sich im selben Verzeichnis wie das Hauptskript):
var Login = erfordern ('./ login.js');… app.get ('/ login', Funktion (req, res) var login = neuer Login (req.param ('Benutzername')), req.param ( 'password)); login.on (' error ', Funktion (error) res.writeHead (500); res.end ();); login.on (' failure ', Funktion (Ursache) if (Ursache == 'Benutzername') res.end ('Falscher Benutzername!'); else if (Grund == 'Passwort') Res.end ('Falsches Passwort!');; login.on (' Erfolg ', Funktion (Daten) req.session.username = req.param (' Benutzername '); req.session.data = Daten; res.redirect (' / userarea ');); login.perform (); );
Hier sind noch ein paar Zeilen Code, aber die Lesbarkeit des Codes hat sich merklich erhöht. Diese Lösung verwendet auch keine externen Bibliotheken. Dies macht es perfekt, wenn jemand neues zu Ihrem Projekt kommt.
Das war der erste Ansatz, gehen wir zum zweiten über.
Versprechen zu verwenden ist eine andere Möglichkeit, dieses Problem zu lösen. Ein Versprechen (wie Sie in dem angegebenen Link nachlesen können) "stellt den eventuellen Wert dar, der von der einmaligen Beendigung einer Operation zurückgegeben wird". In der Praxis bedeutet dies, dass Sie die Aufrufe verketten können, um die Pyramide flach zu machen und den Code leichter lesbar zu machen.
Wir werden das Q-Modul verwenden, das im NPM-Repository verfügbar ist.
Bevor wir beginnen, möchte ich Sie mit Q vertraut machen. Für statische Klassen (Module) verwenden wir hauptsächlich die Q.nfcall
Funktion. Es hilft uns bei der Umwandlung jeder Funktion, die dem Callback-Muster von Node.js folgt (wobei die Parameter des Callbacks der Fehler und das Ergebnis sind), zu einem Versprechen. Es wird so verwendet:
Q.nfcall (http.get, Optionen);
Es ist ziemlich ähnlich Object.prototype.call
. Sie können auch die Q.napp
das ähnelt Object.prototype.apply
:
Q.nfapply (fs.readFile, ['dateiname.txt', 'utf-8']);
Wenn wir das Versprechen erstellen, fügen wir jeden Schritt mit dem hinzu dann (stepCallback)
Methode, fangen Sie die Fehler mit catch (errorCallback)
und beenden mit erledigt()
.
In diesem Fall seit dem sql
object ist eine Instanz, keine statische Klasse, die wir verwenden müssen Q.ninvoke
oder Q.npost
, welche ähnlich wie oben sind. Der Unterschied ist, dass wir den Namen der Methode als Zeichenfolge im ersten Argument übergeben und die Instanz der Klasse, mit der wir arbeiten möchten, als zweite Klasse, um zu vermeiden, dass die Methode verwendet wird ungebunden aus der Instanz.
Als Erstes müssen Sie den ersten Schritt mit Hilfe von ausführen Q.nfcall
oder Q.napp
(Verwenden Sie die, die Sie mehr mögen, darunter ist kein Unterschied):
var Q = required ('q');… app.get ('/ login', Funktion (req, res)) Q.ninvoke ('query', sql, 'SELECT 1 FROM Benutzer WHERE name =?;', [ req.param ('Benutzername')]));
Beachten Sie das Fehlen eines Semikolons am Ende der Zeile - die Funktionsaufrufe werden verkettet, sodass sie nicht dort sein können. Wir rufen nur an sql.query
wie im unordentlichen Beispiel, aber wir lassen den Callback-Parameter weg - er wird durch das Versprechen gehandhabt.
Jetzt können wir den Rückruf für die SQL-Abfrage erstellen. Er ist fast identisch mit dem im Beispiel "Doom Pyramide". Fügen Sie dies nach dem hinzu Q.ninvoke
Anruf:
.dann (Funktion (Zeilen) If (Zeilen.Länge) < 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') ]); )
Wie Sie sehen, fügen wir den Rückruf (den nächsten Schritt) mit der dann
Methode. Auch im Callback lassen wir das weg Error
Parameter, da wir später alle Fehler abfangen werden. Wir prüfen manuell, ob die Abfrage etwas zurückgegeben hat, und wenn ja, geben wir das nächste auszuführende Versprechen zurück (wiederum kein Semikolon aufgrund der Verkettung)..
Wie beim Modularisierungsbeispiel ist die Überprüfung des Kennworts fast identisch mit der Überprüfung des Benutzernamens. Das sollte gleich nach dem letzten gehen dann
Anruf:
.dann (Funktion (Zeilen) If (Zeilen.Länge) < 1) res.end('Wrong password!'); else return Q.ninvoke('query', sql, 'SELECT * FROM userdata WHERE name = ?;', [ req.param('username') ]); )
Der letzte Schritt wird der sein, in dem wir die Daten der Benutzer in die Sitzung einfügen. Wieder einmal unterscheidet sich der Rückruf nicht wesentlich von dem chaotischen Beispiel:
.dann (Funktion (Zeilen) req.session.username = req.param ('Benutzername'); req.session.data = Zeilen [0]; res.rediect ('/ userarea');)
Bei der Verwendung von Versprechungen und der Q-Bibliothek werden alle Fehler vom Rückrufsatz verarbeitet, der die Option verwendet Fang
Methode. Hier senden wir nur den HTTP 500, egal wie der Fehler ist, wie in den obigen Beispielen:
.catch (Funktion (Fehler) res.writeHead (500); res.end ();) .done ();
Danach müssen wir die anrufen erledigt
eine Methode, um sicherzustellen, dass ein Fehler erneut ausgegeben und gemeldet wird, wenn ein Fehler nicht vor dem Ende behandelt wird (aus der README-Bibliothek der Bibliothek). Nun sollte unser schön abgeflachter Code so aussehen (und sich wie der unordentliche verhalten):
var Q = required ('q');… app.get ('/ login', Funktion (req, res)) Q.ninvoke ('query', sql, 'SELECT 1 FROM Benutzer WHERE name =?;', [ req.param ('username')]) .then (Funktion (Zeilen) if (Zeilen.Länge < 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(); );
Der Code ist viel sauberer und erforderte weniger Umschreiben als der Modularisierungsansatz.
Diese Lösung ähnelt der vorherigen, ist jedoch einfacher. Q ist etwas schwer, weil es die ganze verheißende Idee umsetzt. Die Step-Bibliothek dient nur dazu, die Callback-Hölle zu glätten. Es ist auch etwas einfacher zu bedienen, da Sie einfach die einzige Funktion aufrufen, die aus dem Modul exportiert wird, alle Ihre Rückrufe als Parameter übergeben und verwenden diese
anstelle von jedem Rückruf. So kann das chaotische Beispiel mit dem Step-Modul in dieses umgewandelt werden:
var step = required ('step');… app.get ('/ login', Funktion (req, res)) step (Funktion start () sql.query ('SELECT 1 FROM Benutzer WHERE name =?;', [req.param ('Benutzername')], this);, Funktion checkUsername (Fehler, Zeilen) 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'); ); );
Der Nachteil hierbei ist, dass es keinen gemeinsamen Fehlerbehandler gibt. Obwohl alle in einem Rückruf geworfenen Ausnahmen als erster Parameter an den nächsten übergeben werden (das Skript wird daher nicht aufgrund der nicht erfassten Ausnahme heruntergefahren), ist es meistens praktisch, einen Handler für alle Fehler zu haben.
Das ist eine ziemlich persönliche Entscheidung, aber um Ihnen die Wahl zu erleichtern, finden Sie hier eine Liste der Vor- und Nachteile jedes Ansatzes:
Pros:
Nachteile:
Pros:
Nachteile:
Pros:
Nachteile:
Schritt
einwandfrei funktionierenWie Sie sehen, kann die asynchrone Natur von Node.js verwaltet und die Callback-Hölle vermieden werden. Ich persönlich benutze den Modularisierungsansatz, weil ich meinen Code gerne gut strukturiert habe. Ich hoffe, diese Tipps helfen Ihnen, Ihren Code lesbarer zu schreiben und Ihre Skripte einfacher zu debuggen.