Gestion de la nature asynchrone de Node.js

Node.js vous permet de créer des applications rapidement et facilement. Mais en raison de sa nature asynchrone, il peut être difficile d’écrire du code lisible et gérable. Dans cet article, je vais vous montrer quelques conseils pour y parvenir..


Callback Hell ou la pyramide du destin

Node.js est construit de manière à vous obliger à utiliser des fonctions asynchrones. Cela signifie des rappels, des rappels et encore plus de rappels. Vous avez probablement déjà vu ou même écrit des morceaux de code comme ceci:

app.get ('/ login', fonction (req, res) sql.query ('SELECT 1 DES utilisateurs WHERE nom =?;', 'req.param (' nomutilisateur ')], fonction (erreur, lignes)  if (erreur) res.writeHead (500); retourne 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'); );  );  ); );

C’est en fait un extrait directement de l’une de mes premières applications Node.js. Si vous avez fait quelque chose de plus avancé dans Node.js, vous comprenez probablement tout, mais le problème ici est que le code se déplace vers la droite chaque fois que vous utilisez une fonction asynchrone. Il devient plus difficile à lire et à déboguer. Heureusement, il existe quelques solutions à ce gâchis pour vous permettre de choisir celle qui convient à votre projet..


Solution 1: dénomination de rappel et modularisation

La méthode la plus simple consiste à nommer chaque rappel (ce qui vous aidera à déboguer le code) et à le diviser en modules. L'exemple de connexion ci-dessus peut être transformé en un module en quelques étapes simples..

La structure

Commençons par une structure de module simple. Pour éviter la situation ci-dessus, lorsque vous divisez le gâchis en désordre plus petits, organisez-le en classe:

var util = require ('util'); fonction Login (nom d'utilisateur, mot de passe) function _checkForErrors (erreur, lignes, raison)  function _checkUsername (erreur, lignes)  function _checkPassword (erreur, lignes)  function _getData (erreur, lignes)  function perform ()  this.perform = perform;  util.inherits (Login, EventEmitter);

La classe est construite avec deux paramètres: Nom d'utilisateur et mot de passe. En regardant le code exemple, nous avons besoin de trois fonctions: une pour vérifier si le nom d’utilisateur est correct (_checkUsername), une autre pour vérifier le mot de passe (_checkPassword) et un de plus pour renvoyer les données relatives à l'utilisateur (_getData) et informez l'application que la connexion a réussi. Il y a aussi _checkForErrors helper, qui gérera toutes les erreurs. Enfin, il y a un effectuer function, qui lancera la procédure de connexion (et est la seule fonction publique de la classe). Enfin, nous héritons de EventEmitter simplifier l'utilisation de cette classe.

L'assistant

le _checkForErrors La fonction vérifiera si une erreur s'est produite ou si la requête SQL ne renvoie aucune ligne et émettra l'erreur appropriée (avec la raison qui a été fournie):

function _checkForErrors (erreur, lignes, raison) if (erreur) this.emit ('erreur', erreur); retourne vrai;  if (rows.length < 1)  this.emit('failure', reason); return true;  return false; 

Il retourne aussi vrai ou faux, selon qu'une erreur est survenue ou non.

Effectuer la connexion

le effectuer La fonction ne devra effectuer qu'une seule opération: effectuer la première requête SQL (pour vérifier si le nom d'utilisateur existe) et affecter le rappel approprié:

function perform () sql.query ('SELECT 1 DES utilisateurs WHERE name =?;', [nom d'utilisateur], _checkUsername); 

Je suppose que votre connexion SQL est accessible globalement dans le sql variable (juste pour simplifier, discuter du point de savoir s’il s’agit d’une bonne pratique dépasse le cadre de cet article). Et c'est tout pour cette fonction.

Vérification du nom d'utilisateur

L'étape suivante consiste à vérifier si le nom d'utilisateur est correct et, le cas échéant, déclencher la deuxième requête - pour vérifier le mot de passe:

function _checkUsername (erreur, lignes) if (_checkForErrors (erreur, lignes, 'nom d'utilisateur')) return false;  else sql.query ('SELECT 1 FROM utilisateurs WHERE nom =? && mot_de_passe = MD5 (?);', [nom d'utilisateur, mot de passe], _checkPassword); 

À peu près le même code que dans l'exemple désordonné, à l'exception de la gestion des erreurs.

Vérification du mot de passe

Cette fonction est presque exactement la même que la précédente, la seule différence étant la requête appelée:

function _checkPassword (erreur, lignes) if (_checkForErrors (erreur, lignes, 'mot de passe'))) return false;  else sql.query ('SELECT * FROM userdata WHERE name =?;', [nom d'utilisateur], _getData); 

Obtenir les données relatives à l'utilisateur

La dernière fonction de cette classe récupérera les données relatives à l'utilisateur (étape facultative) et déclenchera un événement de réussite avec ce dernier:

fonction _getData (erreur, lignes) if (_checkForErrors (erreur, lignes)) return false;  else this.emit ('success', rows [0]); 

Touches finales et utilisation

La dernière chose à faire est d'exporter la classe. Ajoutez cette ligne après tout le code:

module.exports = Login;

Cela fera la S'identifier classe la seule chose que le module exportera. Il peut être utilisé plus tard comme ceci (en supposant que vous ayez nommé le fichier de module login.js et il se trouve dans le même répertoire que le script principal):

var Login = require ('./ login.js');… app.get ('/ login', fonction (req, res) var login = nouvelle connexion (req.param ('nomutilisateur'), ​​req.param ( 'mot de passe)); login.on (' erreur ', fonction (erreur) res.writeHead (500); res.end ();); login.on (' échec ', fonction (raison) if (raison == 'nom d'utilisateur') res.end ('Nom d'utilisateur incorrect!'); else if (raison == 'mot de passe') res.end ('Mot de passe erroné!');)) login.on (' succès ', fonction (données) req.session.nomutilisateur = req.param (' nomutilisateur '); req.session.data = données; res.redirect (' / userarea ');); login.perform (); );

Voici quelques lignes de code supplémentaires, mais la lisibilité du code a considérablement augmenté. De plus, cette solution n'utilise aucune bibliothèque externe, ce qui la rend parfaite si quelqu'un de nouveau arrive dans votre projet..

Ce fut la première approche, passons à la seconde.


Solution 2: promesses

Utiliser des promesses est un autre moyen de résoudre ce problème. Une promesse (comme vous pouvez le lire dans le lien fourni) "représente la valeur éventuelle renvoyée à la suite de l'achèvement d'une opération". En pratique, cela signifie que vous pouvez chaîner les appels pour aplatir la pyramide et faciliter la lecture du code..

Nous allons utiliser le module Q, disponible dans le référentiel NPM.

Q en bref

Avant de commencer, laissez-moi vous présenter le Q. Pour les classes statiques (modules), nous utiliserons principalement le Q.nfcall une fonction. Cela nous aide dans la conversion de chaque fonction qui suit le modèle de rappel de Node.js (où les paramètres du rappel sont l’erreur et le résultat) en une promesse. C'est utilisé comme ça:

Q.nfcall (http.get, options);

C'est à peu près comme Object.prototype.call. Vous pouvez également utiliser le Q.nfapply qui ressemble Object.prototype.apply:

Q.nfapply (fs.readFile, ['filename.txt', 'utf-8']);

En outre, lorsque nous créons la promesse, nous ajoutons chaque étape avec le puis (stepCallback) méthode, attraper les erreurs avec catch (errorCallback) et finir avec terminé().

Dans ce cas, depuis le sql objet est une instance, pas une classe statique, nous devons utiliser Q.ninvoke ou Q.npost, qui sont similaires à ce qui précède. La différence est que nous passons le nom des méthodes sous forme de chaîne dans le premier argument et l'instance de la classe que nous voulons utiliser comme second, pour éviter que la méthode ne soit non lié de l'instance.

Préparer la promesse

La première chose à faire est d'exécuter la première étape en utilisant Q.nfcall ou Q.nfapply (utilisez celui que vous aimez le plus, il n'y a pas de différence en dessous):

var Q = require ('q');… app.get ('/ login', fonction (req, res) Q.ninvoke ('query', sql, 'SELECT 1 des utilisateurs WHERE name =?;', [ req.param ('nom d'utilisateur')]));

Notez l'absence de point-virgule à la fin de la ligne - les appels de fonction seront chaînés pour qu'il ne puisse pas être là. Nous appelons juste le sql.query comme dans l'exemple désordonné, mais nous omettons le paramètre de rappel - il est géré par la promesse.

Vérification du nom d'utilisateur

Maintenant, nous pouvons créer le rappel pour la requête SQL, il sera presque identique à celui de l'exemple "pyramid of doom". Ajoutez ceci après le Q.ninvoke appel:

.then (function (rows)) if (rows.length < 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') ]);  )

Comme vous pouvez le constater, nous attachons le rappel (étape suivante) à l’aide du puis méthode. De plus, dans le rappel, nous omettons le Erreur paramètre, car nous allons attraper toutes les erreurs plus tard. Nous vérifions manuellement si la requête a renvoyé quelque chose et, le cas échéant, nous retournons la prochaine promesse à exécuter (encore une fois, pas de point-virgule à cause du chaînage)..

Vérification du mot de passe

Comme dans l'exemple de la modularisation, vérifier le mot de passe est presque identique à vérifier le nom d'utilisateur. Cela devrait aller juste après le dernier puis appel:

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

Obtenir les données relatives à l'utilisateur

La dernière étape sera celle où nous mettrons les données des utilisateurs dans la session. Une fois de plus, le rappel n’est pas très différent de l’exemple malpropre:

.then (function (rows) req.session.nomutilisateur = req.param ('username'); req.session.data = lignes [0]; res.rediect ('/ userarea');)

Vérification des erreurs

Lors de l'utilisation de promesses et de la bibliothèque Q, toutes les erreurs sont traitées par le jeu de rappels à l'aide de la commande capture méthode. Ici, nous n’envoyons que le HTTP 500, quelle que soit l’erreur, comme dans les exemples ci-dessus:

.catch (fonction (erreur) res.writeHead (500); res.end ();) .done ();

Après cela, nous devons appeler le terminé méthode pour "s’assurer que, si une erreur n’est pas gérée avant la fin, elle sera retranchée et signalée" (à partir du fichier README de la bibliothèque). Maintenant, notre code magnifiquement aplati devrait ressembler à ceci (et se comporter exactement comme celui qui est en désordre):

var Q = require ('q');… app.get ('/ login', fonction (req, res) Q.ninvoke ('query', sql, 'SELECT 1 des utilisateurs WHERE name =?;', [ req.param ('username')]) .then (function (rows)) if (rows.length < 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(); );

Le code est beaucoup plus propre et implique moins de réécriture que l'approche de la modularisation.


Solution 3: bibliothèque d'étapes

Cette solution est similaire à la précédente mais plus simple. Q est un peu lourd, car il met en œuvre l’idée de promesses. La bibliothèque Step n’est là que pour aplatir l’enfer de rappel. C’est aussi un peu plus simple à utiliser, car vous n’appelez que la seule fonction exportée du module, transmettez tous vos rappels en tant que paramètres et utilisez ce à la place de chaque rappel. Ainsi, l'exemple désordonné peut être converti en cela en utilisant le module Step:

var step = require ('step');… app.get ('/ login', fonction (req, res)) step (function start () sql.query ('SELECT 1 parmi les utilisateurs WHERE nom =?;', [req.param ('username')], this);, fonction checkUsername (erreur, lignes) if (erreur) res.writeHead (500); retourne 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');  ); );

L'inconvénient est qu'il n'y a pas de gestionnaire d'erreur commun. Bien que toutes les exceptions lancées dans un rappel soient transmises au suivant en tant que premier paramètre (pour que le script ne disparaisse pas à cause de l'exception non capturée), il est pratique de disposer d'un gestionnaire pour toutes les erreurs.


Lequel choisir?

C'est un choix plutôt personnel, mais pour vous aider à choisir le bon, voici une liste des avantages et inconvénients de chaque approche:

Modularisation:

Avantages:

  • Pas de librairies externes
  • Aide à rendre le code plus réutilisable

Les inconvénients:

  • Plus de code
  • Beaucoup de réécriture si vous convertissez un projet existant

Promesses (Q):

Avantages:

  • Moins de code
  • Seulement un peu de réécriture si appliqué à un projet existant

Les inconvénients:

  • Vous devez utiliser une bibliothèque externe
  • Nécessite un peu d'apprentissage

Step Library:

Avantages:

  • Facile à utiliser, aucun apprentissage requis
  • Pratiquement copier-coller si vous convertissez un projet existant

Les inconvénients:

  • Aucun gestionnaire d'erreur commun
  • Un peu plus difficile d'indenter que étape fonctionner correctement

Conclusion

Comme vous pouvez le constater, la nature asynchrone de Node.js peut être gérée et l’enfer de rappel peut être évité. Personnellement, j'utilise l'approche de la modularisation, car j'aime bien structurer mon code. J'espère que ces conseils vous aideront à écrire votre code plus lisiblement et à déboguer vos scripts plus facilement.