Les 10 erreurs les plus courantes commises par les développeurs Node.js

Publié: 2022-03-11

Depuis le moment où Node.js a été dévoilé au monde, il a reçu une part équitable d'éloges et de critiques. Le débat se poursuit toujours et pourrait ne pas se terminer de si tôt. Ce que nous oublions souvent dans ces débats, c'est que chaque langage de programmation et chaque plate-forme sont critiqués en fonction de certains problèmes, qui sont créés par la façon dont nous utilisons la plate-forme. Indépendamment de la difficulté avec laquelle Node.js rend l'écriture de code sûr et de la facilité avec laquelle il facilite l'écriture de code hautement simultané, la plate-forme existe depuis un certain temps et a été utilisée pour créer un grand nombre de services Web robustes et sophistiqués. Ces services Web évoluent bien et ont prouvé leur stabilité grâce à leur endurance sur Internet.

Cependant, comme toute autre plate-forme, Node.js est vulnérable aux problèmes et problèmes des développeurs. Certaines de ces erreurs dégradent les performances, tandis que d'autres font apparaître Node.js tout simplement inutilisable pour tout ce que vous essayez de réaliser. Dans cet article, nous examinerons dix erreurs courantes que les développeurs novices de Node.js commettent souvent, et comment elles peuvent être évitées pour devenir un pro de Node.js.

Erreurs du développeur node.js

Erreur n°1 : bloquer la boucle d'événements

JavaScript dans Node.js (tout comme dans le navigateur) fournit un environnement à thread unique. Cela signifie qu'aucune partie de votre application ne s'exécute en parallèle ; à la place, la simultanéité est obtenue grâce à la gestion asynchrone des opérations liées aux E/S. Par exemple, une requête de Node.js au moteur de base de données pour récupérer un document est ce qui permet à Node.js de se concentrer sur une autre partie de l'application :

 // 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 }) 

environnement à thread unique node.js

Cependant, un morceau de code lié au processeur dans une instance Node.js avec des milliers de clients connectés est tout ce qu'il faut pour bloquer la boucle d'événements, faisant attendre tous les clients. Les codes liés au processeur incluent la tentative de tri d'un grand tableau, l'exécution d'une boucle extrêmement longue, etc. Par exemple:

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

Invoquer cette fonction "sortUsersByAge" peut convenir si elle est exécutée sur un petit tableau "utilisateurs", mais avec un grand tableau, cela aura un impact horrible sur les performances globales. Si c'est quelque chose qui doit absolument être fait, et que vous êtes certain qu'il n'y aura rien d'autre en attente sur la boucle d'événements (par exemple, si cela faisait partie d'un outil de ligne de commande que vous construisez avec Node.js, et qu'il n'aurait pas d'importance si tout fonctionnait de manière synchrone), alors cela peut ne pas être un problème. Cependant, dans une instance de serveur Node.js essayant de servir des milliers d'utilisateurs à la fois, un tel modèle peut s'avérer fatal.

Si ce tableau d'utilisateurs était extrait de la base de données, la solution idéale serait de l'extraire déjà trié directement de la base de données. Si la boucle d'événements était bloquée par une boucle écrite pour calculer la somme d'un long historique de données de transactions financières, elle pourrait être reportée à une configuration de travail/file d'attente externe pour éviter de monopoliser la boucle d'événements.

Comme vous pouvez le constater, il n'existe pas de solution miracle à ce type de problème Node.js, mais chaque cas doit être traité individuellement. L'idée fondamentale est de ne pas effectuer de travail intensif sur le processeur dans les instances Node.js frontales - celles auxquelles les clients se connectent simultanément.

Erreur #2 : Invoquer un rappel plus d'une fois

JavaScript s'appuie sur les rappels depuis toujours. Dans les navigateurs Web, les événements sont gérés en transmettant des références à des fonctions (souvent anonymes) qui agissent comme des rappels. Dans Node.js, les rappels étaient le seul moyen pour les éléments asynchrones de votre code de communiquer entre eux - jusqu'à l'introduction des promesses. Les rappels sont toujours utilisés et les développeurs de packages conçoivent toujours leurs API autour des rappels. Un problème courant de Node.js lié à l'utilisation des rappels est de les appeler plusieurs fois. En règle générale, une fonction fournie par un package pour faire quelque chose de manière asynchrone est conçue pour attendre une fonction comme dernier argument, qui est appelée lorsque la tâche asynchrone est terminée :

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

Remarquez qu'il y a une instruction de retour à chaque fois que "done" est appelé, jusqu'à la toute dernière fois. En effet, l'appel du rappel ne met pas automatiquement fin à l'exécution de la fonction en cours. Si le premier "retour" a été commenté, le fait de transmettre un mot de passe non-chaîne à cette fonction entraînera toujours l'appel de "computeHash". Selon la façon dont "computeHash" traite un tel scénario, "done" peut être appelé plusieurs fois. Toute personne utilisant cette fonction d'ailleurs peut être prise complètement au dépourvu lorsque le rappel qu'elle passe est invoqué plusieurs fois.

Il suffit d'être prudent pour éviter cette erreur Node.js. Certains développeurs Node.js ont pour habitude d'ajouter un mot-clé return avant chaque invocation de callback :

 if(err) { return done(err) }

Dans de nombreuses fonctions asynchrones, la valeur de retour n'a presque aucune signification, cette approche permet donc souvent d'éviter facilement un tel problème.

Erreur #3 : Imbriquer profondément les rappels

Les rappels profondément imbriqués, souvent appelés "l'enfer des rappels", ne sont pas un problème Node.js en soi. Cependant, cela peut entraîner des problèmes qui font que le code devient rapidement incontrôlable :

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

L'enfer des rappels

Plus la tâche est complexe, plus cela peut s'aggraver. En imbriquant les rappels de cette manière, nous nous retrouvons facilement avec un code sujet aux erreurs, difficile à lire et difficile à maintenir. Une solution de contournement consiste à déclarer ces tâches en tant que petites fonctions, puis à les lier. Cependant, l'une des solutions (sans doute) les plus propres à cela consiste à utiliser un package utilitaire Node.js qui traite des modèles JavaScript asynchrones, tels que Async.js :

 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() { // ... }) }

Semblable à "async.waterfall", il existe un certain nombre d'autres fonctions fournies par Async.js pour gérer différents modèles asynchrones. Par souci de brièveté, nous avons utilisé ici des exemples plus simples, mais la réalité est souvent pire.

Erreur #4 : S'attendre à ce que les rappels s'exécutent de manière synchrone

La programmation asynchrone avec des rappels n'est peut-être pas quelque chose d'unique à JavaScript et Node.js, mais ils sont responsables de sa popularité. Avec d'autres langages de programmation, nous sommes habitués à l'ordre d'exécution prévisible où deux instructions s'exécuteront l'une après l'autre, à moins qu'il n'y ait une instruction spécifique pour sauter entre les instructions. Même dans ce cas, ceux-ci sont souvent limités aux instructions conditionnelles, aux instructions de boucle et aux invocations de fonctions.

Cependant, en JavaScript, avec les rappels, une fonction particulière peut ne pas s'exécuter correctement tant que la tâche qu'elle attend n'est pas terminée. L'exécution de la fonction en cours se poursuivra jusqu'à la fin sans aucun arrêt :

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

Comme vous le remarquerez, l'appel de la fonction "testTimeout" affichera d'abord "Begin", puis imprimera "Waiting.." suivi du message "Done!" après environ une seconde.

Tout ce qui doit se produire après le déclenchement d'un rappel doit être appelé depuis celui-ci.

Erreur #5 : Attribuer à "exports", au lieu de "module.exports"

Node.js traite chaque fichier comme un petit module isolé. Si votre package contient deux fichiers, peut-être "a.js" et "b.js", alors pour que "b.js" accède à la fonctionnalité de "a.js", "a.js" doit l'exporter en ajoutant des propriétés à l'objet exports :

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

Lorsque cela est fait, toute personne nécessitant "a.js" recevra un objet avec la fonction de propriété "verifyPassword":

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

Cependant, que se passe-t-il si nous voulons exporter cette fonction directement, et non comme la propriété d'un objet ? Nous pouvons écraser les exportations pour ce faire, mais nous ne devons pas le traiter comme une variable globale alors :

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

Remarquez comment nous traitons les "exportations" comme une propriété de l'objet module. La distinction ici entre "module.exports" et "exports" est très importante, et est souvent une cause de frustration chez les nouveaux développeurs Node.js.

Erreur #6 : lancer des erreurs à partir de rappels internes

JavaScript a la notion d'exceptions. Imitant la syntaxe de presque tous les langages traditionnels avec prise en charge de la gestion des exceptions, tels que Java et C++, JavaScript peut « lancer » et intercepter des exceptions dans des blocs try-catch :

 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!') }

Cependant, try-catch ne se comportera pas comme on pourrait s'y attendre dans des situations asynchrones. Par exemple, si vous vouliez protéger un gros morceau de code avec beaucoup d'activité asynchrone avec un gros bloc try-catch, cela ne fonctionnerait pas nécessairement :

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

Si le rappel de "db.User.get" s'est déclenché de manière asynchrone, la portée contenant le bloc try-catch serait depuis longtemps sortie de son contexte pour qu'elle puisse toujours intercepter les erreurs générées depuis l'intérieur du rappel.

C'est ainsi que les erreurs sont gérées d'une manière différente dans Node.js, et cela rend essentiel de suivre le modèle (err, …) sur tous les arguments de la fonction de rappel - le premier argument de tous les rappels devrait être une erreur si une se produit .

Erreur #7 : supposer que le nombre est un type de données entier

Les nombres en JavaScript sont des virgules flottantes - il n'y a pas de type de données entier. Vous ne vous attendriez pas à ce que ce soit un problème, car les nombres suffisamment grands pour souligner les limites du flottement ne sont pas souvent rencontrés. C'est exactement à ce moment-là que les erreurs liées à cela se produisent. Étant donné que les nombres à virgule flottante ne peuvent contenir que des représentations entières jusqu'à une certaine valeur, le dépassement de cette valeur dans tout calcul commencera immédiatement à le gâcher. Aussi étrange que cela puisse paraître, ce qui suit est évalué à vrai dans Node.js :

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

Malheureusement, les bizarreries avec les nombres en JavaScript ne s'arrêtent pas là. Même si les nombres sont des virgules flottantes, les opérateurs qui fonctionnent sur des types de données entiers fonctionnent également ici :

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

Cependant, contrairement aux opérateurs arithmétiques, les opérateurs au niveau du bit et les opérateurs de décalage ne fonctionnent que sur les 32 bits de fin de ces grands nombres "entiers". Par exemple, essayer de décaler "Math.pow(2, 53)" de 1 sera toujours évalué à 0. Essayer de faire un bitwise-or de 1 avec ce même grand nombre sera évalué à 1.

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

Vous aurez peut-être rarement besoin de traiter de grands nombres, mais si vous le faites, il existe de nombreuses grandes bibliothèques d'entiers qui implémentent les opérations mathématiques importantes sur des nombres de grande précision, comme node-bigint.

Erreur #8 : Ignorer les avantages des API de streaming

Supposons que nous souhaitions créer un petit serveur Web de type proxy qui réponde aux demandes en récupérant le contenu d'un autre serveur Web. À titre d'exemple, nous allons construire un petit serveur Web qui sert des images Gravatar :

 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)

Dans cet exemple particulier de problème Node.js, nous récupérons l'image de Gravatar, la lisons dans un tampon, puis répondons à la requête. Ce n'est pas une si mauvaise chose à faire, étant donné que les images Gravatar ne sont pas trop grandes. Cependant, imaginez si la taille du contenu que nous utilisons par procuration était de plusieurs milliers de mégaoctets. Une bien meilleure approche aurait été celle-ci :

 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)

Ici, nous récupérons l'image et transmettons simplement la réponse au client. À aucun moment, nous n'avons besoin de lire l'intégralité du contenu dans un tampon avant de le servir.

Erreur #9 : Utiliser Console.log à des fins de débogage

Dans Node.js, "console.log" vous permet d'imprimer presque n'importe quoi sur la console. Passez-lui un objet et il l'imprimera comme un littéral d'objet JavaScript. Il accepte n'importe quel nombre arbitraire d'arguments et les imprime tous soigneusement séparés par des espaces. Il existe un certain nombre de raisons pour lesquelles un développeur peut être tenté de l'utiliser pour déboguer son code ; cependant, il est fortement recommandé d'éviter "console.log" dans le code réel. Vous devez éviter d'écrire "console.log" partout dans le code pour le déboguer, puis de les commenter lorsqu'ils ne sont plus nécessaires. Au lieu de cela, utilisez l'une des incroyables bibliothèques conçues spécialement pour cela, telles que debug.

Des packages comme ceux-ci fournissent des moyens pratiques d'activer et de désactiver certaines lignes de débogage lorsque vous démarrez l'application. Par exemple, avec le débogage, il est possible d'empêcher toute ligne de débogage d'être imprimée sur le terminal en ne définissant pas la variable d'environnement DEBUG. Son utilisation est simple :

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

Pour activer les lignes de débogage, exécutez simplement ce code avec la variable d'environnement DEBUG définie sur "app" ou "*":

 DEBUG=app node app.js

Erreur #10 : Ne pas utiliser les programmes de superviseur

Que votre code Node.js soit exécuté en production ou dans votre environnement de développement local, un moniteur de programme superviseur capable d'orchestrer votre programme est extrêmement utile. Une pratique souvent recommandée par les développeurs qui conçoivent et implémentent des applications modernes recommande que votre code échoue rapidement. Si une erreur inattendue survient, n'essayez pas de la gérer, laissez plutôt votre programme planter et demandez à un superviseur de le redémarrer en quelques secondes. Les avantages des programmes de supervision ne se limitent pas au redémarrage des programmes en panne. Ces outils vous permettent de redémarrer le programme en cas de plantage, ainsi que de le redémarrer lorsque certains fichiers changent. Cela rend le développement de programmes Node.js beaucoup plus agréable.

Il existe une pléthore de programmes de supervision disponibles pour Node.js. Par exemple:

  • pm2

  • toujours

  • Nodemon

  • superviseur

Tous ces outils ont leurs avantages et leurs inconvénients. Certains d'entre eux sont bons pour gérer plusieurs applications sur la même machine, tandis que d'autres sont meilleurs pour la gestion des journaux. Cependant, si vous souhaitez démarrer avec un tel programme, tous ces choix sont équitables.

Conclusion

Comme vous pouvez le constater, certains de ces problèmes de Node.js peuvent avoir des effets dévastateurs sur votre programme. Certains peuvent être la cause de la frustration lorsque vous essayez d'implémenter les choses les plus simples dans Node.js. Bien que Node.js ait rendu le démarrage extrêmement facile pour les nouveaux arrivants, il y a toujours des domaines où il est tout aussi facile de se tromper. Les développeurs d'autres langages de programmation peuvent être en mesure de comprendre certains de ces problèmes, mais ces erreurs sont assez courantes chez les nouveaux développeurs Node.js. Heureusement, ils sont faciles à éviter. J'espère que ce petit guide aidera les débutants à écrire un meilleur code dans Node.js et à développer un logiciel stable et efficace pour nous tous.