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..
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..
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..
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.
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.
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.
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.
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);
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]);
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.
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.
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.
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.
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)..
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') ]); )
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');)
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.
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.
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:
Avantages:
Les inconvénients:
Avantages:
Les inconvénients:
Avantages:
Les inconvénients:
étape
fonctionner correctementComme 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.