Verwalten der asynchronen Natur von Node.js

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.


Callback Hell oder die Pyramide des Schicksals

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.


Lösung 1: Callback-Benennung und -Modularisierung

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.

Die Struktur

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.

Der Helfer

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.

Login durchführen

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.

Überprüfen des Benutzernamens

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.

Passwort überprüfen

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); 

Abrufen der benutzerbezogenen Daten

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]); 

Letzte Berührungen und Verwendung

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.


Lösung 2: Versprechen

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.

Q in der Nussschale

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.

Versprechen vorbereiten

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.

Überprüfen des Benutzernamens

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)..

Passwort überprüfen

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') ]);  )

Abrufen der benutzerbezogenen Daten

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');)

Auf Fehler prüfen

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.


Lösung 3: Schrittbibliothek

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.


Welches zu wählen?

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:

Modularisierung:

Pros:

  • Keine externen Bibliotheken
  • Hilft, den Code wiederverwendbar zu machen

Nachteile:

  • Mehr code
  • Viele Umschreibungen, wenn Sie ein vorhandenes Projekt konvertieren

Versprechen (Q):

Pros:

  • Weniger Code
  • Nur ein kleines Umschreiben, wenn auf ein vorhandenes Projekt angewendet

Nachteile:

  • Sie müssen eine externe Bibliothek verwenden
  • Erfordert ein bisschen Lernen

Schrittbibliothek:

Pros:

  • Einfach zu bedienen, kein Lernen erforderlich
  • Ziemlich kopieren und einfügen, wenn Sie ein vorhandenes Projekt konvertieren

Nachteile:

  • Kein allgemeiner Fehlerhandler
  • Ein bisschen schwieriger, das einzurücken Schritt einwandfrei funktionieren

Fazit

Wie 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.