Die 10 häufigsten Fehler, die Node.js-Entwickler machen

Veröffentlicht: 2022-03-11

Seit Node.js der Welt vorgestellt wurde, hat es viel Lob und Kritik erfahren. Die Debatte dauert noch an und wird möglicherweise nicht so schnell enden. Was wir in diesen Debatten oft übersehen, ist, dass jede Programmiersprache und Plattform aufgrund bestimmter Probleme kritisiert wird, die durch die Art und Weise entstehen, wie wir die Plattform nutzen. Unabhängig davon, wie schwierig Node.js das Schreiben von sicherem Code macht und wie einfach es das Schreiben von hochgradig nebenläufigem Code macht, die Plattform gibt es schon seit geraumer Zeit und wurde zum Erstellen einer großen Anzahl robuster und ausgeklügelter Webdienste verwendet. Diese Webdienste lassen sich gut skalieren und haben ihre Stabilität durch ihre lange Lebensdauer im Internet bewiesen.

Wie jede andere Plattform ist Node.js jedoch anfällig für Entwicklerprobleme und -probleme. Einige dieser Fehler beeinträchtigen die Leistung, während andere Node.js für das, was Sie erreichen möchten, direkt unbrauchbar erscheinen lassen. In diesem Artikel werfen wir einen Blick auf zehn häufige Fehler, die Node.js-Neulinge oft machen, und wie sie vermieden werden können, um ein Node.js-Profi zu werden.

node.js-Entwicklerfehler

Fehler Nr. 1: Blockieren der Ereignisschleife

JavaScript in Node.js (genau wie im Browser) bietet eine Single-Thread-Umgebung. Das bedeutet, dass keine zwei Teile Ihrer Anwendung parallel laufen; Stattdessen wird die Parallelität durch die asynchrone Verarbeitung von E/A-gebundenen Vorgängen erreicht. Eine Anfrage von Node.js an die Datenbank-Engine zum Abrufen eines Dokuments ermöglicht es beispielsweise Node.js, sich auf einen anderen Teil der Anwendung zu konzentrieren:

 // Trying to fetch an user object from the database. Node.js is free to run other parts of the code from the moment this function is invoked.. db.User.get(userId, function(err, user) { // .. until the moment the user object has been retrieved here }) 

node.js Single-Thread-Umgebung

Ein Stück CPU-gebundener Code in einer Node.js-Instanz mit Tausenden von verbundenen Clients reicht jedoch aus, um die Ereignisschleife zu blockieren und alle Clients warten zu lassen. CPU-gebundene Codes umfassen den Versuch, ein großes Array zu sortieren, eine extrem lange Schleife auszuführen und so weiter. Zum Beispiel:

 function sortUsersByAge(users) { users.sort(function(a, b) { return a.age < b.age ? -1 : 1 }) }

Der Aufruf dieser „sortUsersByAge“-Funktion mag in Ordnung sein, wenn sie auf einem kleinen „users“-Array ausgeführt wird, aber bei einem großen Array hat dies einen schrecklichen Einfluss auf die Gesamtleistung. Wenn dies unbedingt erforderlich ist und Sie sicher sind, dass nichts anderes auf die Ereignisschleife wartet (z. B. wenn dies Teil eines Befehlszeilentools war, das Sie mit Node.js erstellen, und es egal, ob das Ganze synchron läuft), dann ist das vielleicht kein Problem. In einer Node.js-Serverinstanz, die versucht, Tausende von Benutzern gleichzeitig zu bedienen, kann sich ein solches Muster jedoch als fatal erweisen.

Wenn dieses Array von Benutzern aus der Datenbank abgerufen würde, wäre die ideale Lösung, es bereits sortiert direkt aus der Datenbank abzurufen. Wenn die Ereignisschleife durch eine Schleife blockiert wurde, die geschrieben wurde, um die Summe einer langen Historie von Finanztransaktionsdaten zu berechnen, könnte sie auf ein externes Worker-/Warteschlangen-Setup verschoben werden, um zu vermeiden, dass die Ereignisschleife in Beschlag genommen wird.

Wie Sie sehen können, gibt es für diese Art von Node.js-Problem keine Patentlösung, sondern jeder Fall muss individuell angegangen werden. Die Grundidee besteht darin, keine CPU-intensive Arbeit in den nach vorne gerichteten Node.js-Instanzen zu erledigen – denjenigen, mit denen sich Clients gleichzeitig verbinden.

Fehler Nr. 2: Aufrufen eines Rückrufs mehr als einmal

JavaScript ist seit jeher auf Callbacks angewiesen. In Webbrowsern werden Ereignisse behandelt, indem Verweise auf (häufig anonyme) Funktionen übergeben werden, die wie Callbacks fungieren. In Node.js waren Callbacks früher die einzige Möglichkeit, asynchrone Elemente Ihres Codes miteinander zu kommunizieren – bis Promises eingeführt wurden. Rückrufe werden immer noch verwendet, und Paketentwickler entwerfen ihre APIs immer noch um Rückrufe herum. Ein häufiges Node.js-Problem im Zusammenhang mit der Verwendung von Rückrufen besteht darin, sie mehr als einmal aufzurufen. Typischerweise erwartet eine Funktion, die von einem Paket bereitgestellt wird, um etwas asynchron zu tun, eine Funktion als letztes Argument, das aufgerufen wird, wenn die asynchrone Aufgabe abgeschlossen ist:

 module.exports.verifyPassword = function(user, password, done) { if(typeof password !== 'string') { done(new Error('password should be a string')) return } computeHash(password, user.passwordHashOpts, function(err, hash) { if(err) { done(err) return } done(null, hash === user.passwordHash) }) }

Beachten Sie, dass jedes Mal, wenn „done“ aufgerufen wird, bis zum allerletzten Mal eine return-Anweisung vorhanden ist. Dies liegt daran, dass der Aufruf des Callbacks die Ausführung der aktuellen Funktion nicht automatisch beendet. Wenn das erste „return“ auskommentiert wurde, führt die Übergabe eines Nicht-String-Passworts an diese Funktion immer noch dazu, dass „computeHash“ aufgerufen wird. Je nachdem, wie „computeHash“ mit einem solchen Szenario umgeht, kann „done“ mehrfach aufgerufen werden. Jeder, der diese Funktion an anderer Stelle verwendet, kann völlig unvorbereitet erwischt werden, wenn der von ihm übergebene Rückruf mehrmals aufgerufen wird.

Vorsicht ist alles, was Sie brauchen, um diesen Node.js-Fehler zu vermeiden. Einige Node.js-Entwickler übernehmen die Angewohnheit, vor jedem Callback-Aufruf ein return-Schlüsselwort hinzuzufügen:

 if(err) { return done(err) }

Bei vielen asynchronen Funktionen hat der Rückgabewert fast keine Bedeutung, daher lässt sich mit diesem Ansatz ein solches Problem oft leicht vermeiden.

Fehler Nr. 3: Callbacks tief verschachteln

Tief verschachtelte Callbacks, die oft als „Callback-Hölle“ bezeichnet werden, sind an sich kein Node.js-Problem. Dies kann jedoch zu Problemen führen, die dazu führen, dass der Code schnell außer Kontrolle gerät:

 function handleLogin(..., done) { db.User.get(..., function(..., user) { if(!user) { return done(null, 'failed to log in') } utils.verifyPassword(..., function(..., okay) { if(okay) { return done(null, 'failed to log in') } session.login(..., function() { done(null, 'logged in') }) }) }) } 

Callback-Hölle

Je komplexer die Aufgabe, desto schlimmer kann dies werden. Indem wir Callbacks auf diese Weise verschachteln, erhalten wir leicht fehleranfälligen, schwer lesbaren und schwer zu wartenden Code. Eine Problemumgehung besteht darin, diese Aufgaben als kleine Funktionen zu deklarieren und sie dann zu verknüpfen. Eine der (wohl) saubersten Lösungen dafür ist die Verwendung eines Dienstprogramms Node.js-Paket, das sich mit asynchronen JavaScript-Mustern wie Async.js befasst:

 function handleLogin(done) { async.waterfall([ function(done) { db.User.get(..., done) }, function(user, done) { if(!user) { return done(null, 'failed to log in') } utils.verifyPassword(..., function(..., okay) { done(null, user, okay) }) }, function(user, okay, done) { if(okay) { return done(null, 'failed to log in') } session.login(..., function() { done(null, 'logged in') }) } ], function() { // ... }) }

Ähnlich wie bei „async.waterfall“ gibt es eine Reihe weiterer Funktionen, die Async.js bereitstellt, um mit verschiedenen asynchronen Mustern umzugehen. Der Kürze halber haben wir hier einfachere Beispiele verwendet, aber die Realität ist oft schlimmer.

Fehler Nr. 4: Zu erwarten, dass Callbacks synchron ausgeführt werden

Asynchrones Programmieren mit Callbacks ist vielleicht kein Alleinstellungsmerkmal von JavaScript und Node.js, aber sie sind für ihre Popularität verantwortlich. Bei anderen Programmiersprachen sind wir an die vorhersehbare Ausführungsreihenfolge gewöhnt, bei der zwei Anweisungen nacheinander ausgeführt werden, es sei denn, es gibt eine bestimmte Anweisung zum Springen zwischen Anweisungen. Selbst dann sind diese oft auf bedingte Anweisungen, Schleifenanweisungen und Funktionsaufrufe beschränkt.

In JavaScript kann es jedoch vorkommen, dass eine bestimmte Funktion mit Rückrufen nicht gut ausgeführt wird, bis die Aufgabe, auf die sie wartet, abgeschlossen ist. Die Ausführung der aktuellen Funktion wird ohne Unterbrechung bis zum Ende ausgeführt:

 function testTimeout() { console.log(“Begin”) setTimeout(function() { console.log(“Done!”) }, duration * 1000) console.log(“Waiting..”) }

Wie Sie feststellen werden, wird beim Aufrufen der Funktion „testTimeout“ zuerst „Begin“ und dann „Waiting..“ gedruckt, gefolgt von der Meldung „Done!“. nach etwa einer Sekunde.

Alles, was passieren muss, nachdem ein Callback ausgelöst wurde, muss von dort aus aufgerufen werden.

Fehler Nr. 5: Zuweisen zu „exports“, statt „module.exports“

Node.js behandelt jede Datei als kleines isoliertes Modul. Wenn Ihr Paket zwei Dateien hat, vielleicht „a.js“ und „b.js“, dann muss „a.js“ es exportieren, indem „a.js“ Eigenschaften hinzufügt, damit „b.js“ auf die Funktionalität von „a.js“ zugreifen kann das Exportobjekt:

 // a.js exports.verifyPassword = function(user, password, done) { ... }

Wenn dies erledigt ist, erhält jeder, der „a.js“ benötigt, ein Objekt mit der Eigenschaftsfunktion „verifyPassword“:

 // b.js require('a.js') // { verifyPassword: function(user, password, done) { ... } }

Was aber, wenn wir diese Funktion direkt und nicht als Eigenschaft eines Objekts exportieren möchten? Dazu können wir exports überschreiben, dürfen es dann aber nicht als globale Variable behandeln:

 // a.js module.exports = function(user, password, done) { ... }

Beachten Sie, wie wir „exports“ als Eigenschaft des Modulobjekts behandeln. Die Unterscheidung zwischen „module.exports“ und „exports“ ist hier sehr wichtig und sorgt bei neuen Node.js-Entwicklern oft für Frustration.

Fehler Nr. 6: Fehler aus Inside Callbacks werfen

JavaScript hat den Begriff der Ausnahmen. JavaScript imitiert die Syntax fast aller traditionellen Sprachen mit Unterstützung für Ausnahmebehandlung wie Java und C++ und kann Ausnahmen in Try-Catch-Blöcken „werfen“ und abfangen:

 function slugifyUsername(username) { if(typeof username === 'string') { throw new TypeError('expected a string username, got '+(typeof username)) } // ... } try { var usernameSlug = slugifyUsername(username) } catch(e) { console.log('Oh no!') }

Try-Catch verhält sich jedoch nicht so, wie Sie es in asynchronen Situationen erwarten würden. Wenn Sie beispielsweise einen großen Codeabschnitt mit vielen asynchronen Aktivitäten mit einem großen Try-Catch-Block schützen möchten, würde dies nicht unbedingt funktionieren:

 try { db.User.get(userId, function(err, user) { if(err) { throw err } // ... usernameSlug = slugifyUsername(user.username) // ... }) } catch(e) { console.log('Oh no!') }

Wenn der Rückruf an „db.User.get“ asynchron ausgelöst würde, wäre der Bereich, der den try-catch-Block enthält, schon lange aus dem Zusammenhang gerissen, um diese Fehler, die innerhalb des Rückrufs ausgelöst werden, noch abzufangen.

Auf diese Weise werden Fehler in Node.js anders behandelt, und das macht es unerlässlich, das (err, …)-Muster bei allen Callback-Funktionsargumenten zu befolgen – das erste Argument aller Callbacks wird voraussichtlich ein Fehler sein, wenn einer auftritt .

Fehler Nr. 7: Angenommen, Number sei ein Integer-Datentyp

Zahlen in JavaScript sind Fließkommazahlen – es gibt keinen Integer-Datentyp. Sie würden nicht erwarten, dass dies ein Problem darstellt, da Zahlen, die groß genug sind, um die Grenzen von Float zu betonen, nicht oft vorkommen. Genau dann passieren diesbezügliche Fehler. Da Gleitkommazahlen nur ganzzahlige Darstellungen bis zu einem bestimmten Wert enthalten können, wird das Überschreiten dieses Werts in einer Berechnung sofort zu einem Durcheinander führen. So seltsam es auch scheinen mag, das Folgende wird in Node.js als wahr ausgewertet:

 Math.pow(2, 53)+1 === Math.pow(2, 53)

Leider enden die Macken mit Zahlen in JavaScript hier nicht. Auch wenn Zahlen Fließkommazahlen sind, funktionieren hier auch Operatoren, die mit ganzzahligen Datentypen arbeiten:

 5 % 2 === 1 // true 5 >> 1 === 2 // true

Im Gegensatz zu arithmetischen Operatoren funktionieren bitweise Operatoren und Shift-Operatoren jedoch nur mit den nachgestellten 32 Bits solch großer „ganzzahliger“ Zahlen. Wenn Sie beispielsweise versuchen, „Math.pow(2, 53)“ um 1 zu verschieben, wird immer 0 ausgewertet. Der Versuch, ein bitweises Oder von 1 mit derselben großen Zahl auszuführen, wird zu 1 ausgewertet.

 Math.pow(2, 53) / 2 === Math.pow(2, 52) // true Math.pow(2, 53) >> 1 === 0 // true Math.pow(2, 53) | 1 === 1 // true

Möglicherweise müssen Sie selten mit großen Zahlen umgehen, aber wenn Sie dies tun, gibt es viele große Integer-Bibliotheken, die die wichtigen mathematischen Operationen für Zahlen mit hoher Genauigkeit implementieren, wie z. B. node-bigint.

Fehler Nr. 8: Ignorieren der Vorteile von Streaming-APIs

Angenommen, wir möchten einen kleinen Proxy-ähnlichen Webserver erstellen, der Antworten auf Anfragen bereitstellt, indem er den Inhalt von einem anderen Webserver abruft. Als Beispiel bauen wir einen kleinen Webserver, der Gravatar-Bilder bereitstellt:

 var http = require('http') var crypto = require('crypto') http.createServer() .on('request', function(req, res) { var email = req.url.substr(req.url.lastIndexOf('/')+1) if(!email) { res.writeHead(404) return res.end() } var buf = new Buffer(1024*1024) http.get('http://www.gravatar.com/avatar/'+crypto.createHash('md5').update(email).digest('hex'), function(resp) { var size = 0 resp.on('data', function(chunk) { chunk.copy(buf, size) size += chunk.length }) .on('end', function() { res.write(buf.slice(0, size)) res.end() }) }) }) .listen(8080)

In diesem speziellen Beispiel eines Node.js-Problems holen wir das Bild von Gravatar, lesen es in einen Puffer und antworten dann auf die Anfrage. Dies ist keine schlechte Sache, da Gravatar-Bilder nicht zu groß sind. Stellen Sie sich jedoch vor, die Größe der Inhalte, die wir als Proxy verwenden, wäre Tausende von Megabytes. Ein viel besserer Ansatz wäre dieser gewesen:

 http.createServer() .on('request', function(req, res) { var email = req.url.substr(req.url.lastIndexOf('/')+1) if(!email) { res.writeHead(404) return res.end() } http.get('http://www.gravatar.com/avatar/'+crypto.createHash('md5').update(email).digest('hex'), function(resp) { resp.pipe(res) }) }) .listen(8080)

Hier rufen wir das Bild ab und leiten die Antwort einfach an den Client weiter. Zu keinem Zeitpunkt müssen wir den gesamten Inhalt in einen Puffer einlesen, bevor wir ihn bereitstellen.

Fehler Nr. 9: Verwendung von Console.log für Debugging-Zwecke

In Node.js können Sie mit „console.log“ fast alles auf der Konsole drucken. Übergeben Sie ihm ein Objekt und es wird es als JavaScript-Objektliteral ausgeben. Es akzeptiert eine beliebige Anzahl von Argumenten und gibt sie alle ordentlich durch Leerzeichen getrennt aus. Es gibt eine Reihe von Gründen, warum ein Entwickler versucht sein könnte, dies zum Debuggen seines Codes zu verwenden. Es wird jedoch dringend empfohlen, „console.log“ im echten Code zu vermeiden. Sie sollten vermeiden, „console.log“ über den gesamten Code zu schreiben, um ihn zu debuggen, und sie dann auszukommentieren, wenn sie nicht mehr benötigt werden. Verwenden Sie stattdessen eine der erstaunlichen Bibliotheken, die nur dafür gebaut wurden, wie z. B. debug.

Pakete wie diese bieten bequeme Möglichkeiten zum Aktivieren und Deaktivieren bestimmter Debug-Zeilen, wenn Sie die Anwendung starten. Beispielsweise ist es mit debug möglich, zu verhindern, dass Debug-Zeilen an das Terminal ausgegeben werden, indem die Umgebungsvariable DEBUG nicht gesetzt wird. Die Verwendung ist einfach:

 // app.js var debug = require('debug')('app') debug('Hello, %s!', 'world')

Um Debug-Zeilen zu aktivieren, führen Sie einfach diesen Code aus, wobei die Umgebungsvariable DEBUG auf „app“ oder „*“ gesetzt ist:

 DEBUG=app node app.js

Fehler Nr. 10: Verwenden Sie keine Supervisor-Programme

Unabhängig davon, ob Ihr Node.js-Code in der Produktion oder in Ihrer lokalen Entwicklungsumgebung ausgeführt wird, ist ein Supervisor-Programmmonitor, der Ihr Programm orchestrieren kann, eine äußerst nützliche Sache. Eine Vorgehensweise, die häufig von Entwicklern empfohlen wird, die moderne Anwendungen entwerfen und implementieren, empfiehlt, dass Ihr Code schnell fehlschlagen sollte. Wenn ein unerwarteter Fehler auftritt, versuchen Sie nicht, ihn zu beheben, sondern lassen Sie Ihr Programm abstürzen und lassen Sie es in wenigen Sekunden von einem Supervisor neu starten. Die Vorteile von Supervisor-Programmen beschränken sich nicht nur auf den Neustart abgestürzter Programme. Mit diesen Tools können Sie das Programm bei einem Absturz neu starten und sie neu starten, wenn sich einige Dateien ändern. Dies macht die Entwicklung von Node.js-Programmen zu einer viel angenehmeren Erfahrung.

Es gibt eine Fülle von Supervisor-Programmen für Node.js. Zum Beispiel:

  • pm2

  • für immer

  • Knotenmon

  • Aufsicht

Alle diese Tools haben ihre Vor- und Nachteile. Einige von ihnen eignen sich gut für die Handhabung mehrerer Anwendungen auf demselben Computer, während andere besser in der Protokollverwaltung sind. Wenn Sie jedoch mit einem solchen Programm beginnen möchten, sind all dies faire Entscheidungen.

Fazit

Wie Sie sehen, können einige dieser Node.js-Probleme verheerende Auswirkungen auf Ihr Programm haben. Einige können die Ursache für Frustration sein, während Sie versuchen, die einfachsten Dinge in Node.js zu implementieren. Obwohl Node.js den Einstieg für Neulinge extrem einfach gemacht hat, gibt es immer noch Bereiche, in denen es genauso leicht zu vermasseln ist. Entwickler anderer Programmiersprachen können sich vielleicht auf einige dieser Probleme beziehen, aber diese Fehler sind bei neuen Node.js-Entwicklern recht häufig. Glücklicherweise sind sie leicht zu vermeiden. Ich hoffe, dass diese kurze Anleitung Anfängern hilft, besseren Code in Node.js zu schreiben und stabile und effiziente Software für uns alle zu entwickeln.