Remplacement des rappels par des promesses dans Node.js

94

J'ai un module de nœud simple qui se connecte à une base de données et possède plusieurs fonctions pour recevoir des données, par exemple cette fonction:


dbConnection.js:

import mysql from 'mysql';

const connection = mysql.createConnection({
  host: 'localhost',
  user: 'user',
  password: 'password',
  database: 'db'
});

export default {
  getUsers(callback) {
    connection.connect(() => {
      connection.query('SELECT * FROM Users', (err, result) => {
        if (!err){
          callback(result);
        }
      });
    });
  }
};

Le module serait appelé de cette façon à partir d'un module de nœud différent:


app.js:

import dbCon from './dbConnection.js';

dbCon.getUsers(console.log);

Je voudrais utiliser des promesses au lieu de rappels pour renvoyer les données. Jusqu'à présent, j'ai lu sur les promesses imbriquées dans le fil suivant: Ecrire du code propre avec des promesses imbriquées , mais je n'ai pas trouvé de solution assez simple pour ce cas d'utilisation. Quelle serait la bonne façon de revenir en resultutilisant une promesse?

Lior Erez
la source
1
Voir Adapting Node , si vous utilisez la bibliothèque Q de kriskowal.
Bertrand Marron
1
duplication possible de Comment convertir une API de rappel existante en promesses? Veuillez préciser votre question, ou je vais la fermer
Bergi
@ leo.249: Avez-vous lu la documentation Q? Avez-vous déjà essayé de l'appliquer à votre code - si oui, veuillez poster votre tentative (même si cela ne fonctionne pas)? Où êtes-vous exactement coincé? Vous semblez avoir trouvé une solution non simple, veuillez la poster.
Bergi
3
@ leo.249 Q n'est pratiquement pas maintenu - le dernier commit remonte à 3 mois. Seule la branche v2 est intéressante pour les développeurs Q et ce n'est même pas près d'être prêt pour la production de toute façon. Il y a des problèmes non résolus sans commentaires dans le suivi des problèmes d'octobre. Je vous suggère fortement d'envisager une bibliothèque de promesses bien entretenue.
Benjamin Gruenbaum
2
Super lié Comment convertir une API de rappel en promesses
Benjamin Gruenbaum

Réponses:

102

Utiliser la Promiseclasse

Je recommande de jeter un œil aux documents Promise de MDN qui offrent un bon point de départ pour utiliser Promises. Alternativement, je suis sûr qu'il existe de nombreux tutoriels disponibles en ligne. :)

Remarque: les navigateurs modernes prennent déjà en charge la spécification ECMAScript 6 des promesses (voir les documents MDN liés ci-dessus) et je suppose que vous souhaitez utiliser l'implémentation native, sans bibliothèques tierces.

Quant à un exemple concret ...

Le principe de base fonctionne comme ceci:

  1. Votre API s'appelle
  2. Vous créez un nouvel objet Promise, cet objet prend une seule fonction comme paramètre de constructeur
  3. Votre fonction fournie est appelée par l'implémentation sous-jacente et la fonction reçoit deux fonctions - resolveetreject
  4. Une fois que vous avez fait votre logique, vous appelez l'un d'entre eux pour remplir la promesse ou la rejeter avec une erreur

Cela peut sembler beaucoup alors voici un exemple réel.

exports.getUsers = function getUsers () {
  // Return the Promise right away, unless you really need to
  // do something before you create a new Promise, but usually
  // this can go into the function below
  return new Promise((resolve, reject) => {
    // reject and resolve are functions provided by the Promise
    // implementation. Call only one of them.

    // Do your logic here - you can do WTF you want.:)
    connection.query('SELECT * FROM Users', (err, result) => {
      // PS. Fail fast! Handle errors first, then move to the
      // important stuff (that's a good practice at least)
      if (err) {
        // Reject the Promise with an error
        return reject(err)
      }

      // Resolve (or fulfill) the promise with data
      return resolve(result)
    })
  })
}

// Usage:
exports.getUsers()  // Returns a Promise!
  .then(users => {
    // Do stuff with users
  })
  .catch(err => {
    // handle errors
  })

Utilisation de la fonctionnalité de langage async / await (Node.js> = 7.6)

Dans Node.js 7.6, le compilateur JavaScript v8 a été mis à niveau avec le support async / await . Vous pouvez maintenant déclarer les fonctions comme étant async, ce qui signifie qu'elles renvoient automatiquement un Promisequi est résolu lorsque la fonction asynchrone termine l'exécution. Dans cette fonction, vous pouvez utiliser le awaitmot - clé pour attendre qu'une autre promesse se résout.

Voici un exemple:

exports.getUsers = async function getUsers() {
  // We are in an async function - this will return Promise
  // no matter what.

  // We can interact with other functions which return a
  // Promise very easily:
  const result = await connection.query('select * from users')

  // Interacting with callback-based APIs is a bit more
  // complicated but still very easy:
  const result2 = await new Promise((resolve, reject) => {
    connection.query('select * from users', (err, res) => {
      return void err ? reject(err) : resolve(res)
    })
  })
  // Returning a value will cause the promise to be resolved
  // with that value
  return result
}
Robert Rossmann
la source
14
Les promesses font partie de la spécification ECMAScript 2015 et la v8 utilisée par Node v0.12 fournit la mise en œuvre de cette partie de la spécification. Alors oui, ils ne font pas partie du noyau de Node - ils font partie du langage.
Robert Rossmann
1
Bon à savoir, j'avais l'impression que pour utiliser Promises, vous auriez besoin d'installer un package npm et d'utiliser require (). J'ai trouvé le package promise sur npm qui implémente le style bare bones / A ++ et je l'ai utilisé, mais je suis encore nouveau sur le nœud lui-même (pas JavaScript).
macguru2000
C'est ma façon préférée d'écrire des promesses et du code asynchrone d'architecte, principalement parce qu'il s'agit d'un modèle cohérent, facile à lire et permet un code hautement structuré.
31

Avec bluebird, vous pouvez utiliser Promise.promisifyAll(et Promise.promisify) pour ajouter des méthodes prêtes pour Promise à n'importe quel objet.

var Promise = require('bluebird');
// Somewhere around here, the following line is called
Promise.promisifyAll(connection);

exports.getUsersAsync = function () {
    return connection.connectAsync()
        .then(function () {
            return connection.queryAsync('SELECT * FROM Users')
        });
};

Et utilisez comme ceci:

getUsersAsync().then(console.log);

ou

// Spread because MySQL queries actually return two resulting arguments, 
// which Bluebird resolves as an array.
getUsersAsync().spread(function(rows, fields) {
    // Do whatever you want with either rows or fields.
});

Ajout de broyeurs

Bluebird prend en charge de nombreuses fonctionnalités, l'une d'elles étant les éliminateurs, elle vous permet de disposer en toute sécurité d'une connexion après sa fin à l'aide de Promise.usinget Promise.prototype.disposer. Voici un exemple de mon application:

function getConnection(host, user, password, port) {
    // connection was already promisified at this point

    // The object literal syntax is ES6, it's the equivalent of
    // {host: host, user: user, ... }
    var connection = mysql.createConnection({host, user, password, port});
    return connection.connectAsync()
        // connect callback doesn't have arguments. return connection.
        .return(connection) 
        .disposer(function(connection, promise) { 
            //Disposer is used when Promise.using is finished.
            connection.end();
        });
}

Ensuite, utilisez-le comme ceci:

exports.getUsersAsync = function () {
    return Promise.using(getConnection()).then(function (connection) {
            return connection.queryAsync('SELECT * FROM Users')
        });
};

Cela mettra automatiquement fin à la connexion une fois la promesse résolue avec la valeur (ou rejetée avec un Error).

Fantôme de Madara
la source
3
Excellente réponse, j'ai fini par utiliser bluebird au lieu de Q grâce à vous, merci!
Lior Erez
2
Gardez à l'esprit qu'en utilisant les promesses, vous acceptez d'utiliser try-catchà chaque appel. Donc, si vous le faites assez souvent et que la complexité de votre code est similaire à celle de l'exemple, vous devriez reconsidérer cela.
Andrey Popov
14

Node.js version 8.0.0+:

Vous ne devez pas utiliser Bluebird pour promisify les méthodes de l' API de noeud plus. Parce qu'à partir de la version 8+, vous pouvez utiliser nativement util.promisify :

const util = require('util');

const connectAsync = util.promisify(connection.connectAsync);
const queryAsync = util.promisify(connection.queryAsync);

exports.getUsersAsync = function () {
    return connectAsync()
        .then(function () {
            return queryAsync('SELECT * FROM Users')
        });
};

Maintenant, vous n'avez pas besoin d'utiliser une bibliothèque tierce pour faire la promesse.

asmmahmud
la source
3

En supposant que l'API de votre adaptateur de base de données ne se génère pas d' Promiseselle-même, vous pouvez faire quelque chose comme:

exports.getUsers = function () {
    var promise;
    promise = new Promise();
    connection.connect(function () {
        connection.query('SELECT * FROM Users', function (err, result) {
            if(!err){
                promise.resolve(result);
            } else {
                promise.reject(err);
            }
        });
    });
    return promise.promise();
};

Si l'API de base de données prend en charge, Promisesvous pouvez faire quelque chose comme: (ici, vous voyez la puissance de Promises, votre fluff de rappel disparaît pratiquement)

exports.getUsers = function () {
    return connection.connect().then(function () {
        return connection.query('SELECT * FROM Users');
    });
};

Utilisation .then()pour renvoyer une nouvelle promesse (imbriquée).

Appeler avec:

module.getUsers().done(function (result) { /* your code here */ });

J'ai utilisé une maquette d'API pour mes promesses, votre API peut être différente. Si vous me montrez votre API, je peux l'adapter.

Alcyon
la source
2
Quelle bibliothèque de promesses a un Promiseconstructeur et une .promise()méthode?
Bergi
Je vous remercie. Je pratique juste quelques node.js et ce que j'ai posté était tout ce qu'il y avait à faire, un exemple très simple pour comprendre comment utiliser les promesses. Votre solution semble bonne, mais quel package npm devrais-je installer pour pouvoir l'utiliser promise = new Promise();?
Lior Erez
Bien que votre API renvoie maintenant une promesse, vous ne vous êtes pas débarrassé de la pyramide de malheur, ni n'avez donné un exemple de la façon dont les promesses fonctionnent pour remplacer les rappels.
Le fantôme de Madara
@ leo.249 Je ne sais pas, toute bibliothèque Promise compatible avec Promises / A + devrait être bonne. Voir: promisesaplus.com/@Bergi, ce n'est pas pertinent. @SecondRikudo si l'API avec laquelle vous interagissez ne prend pas en charge, Promisesvous êtes bloqué par l'utilisation de rappels. Une fois que vous entrez dans le territoire promis, la «pyramide» disparaît. Consultez le deuxième exemple de code pour savoir comment cela fonctionnerait.
Halcyon
@Halcyon Voir ma réponse. Même une API existante qui utilise des rappels peut être «promis» en une API prête pour Promise, ce qui aboutit à un code beaucoup plus propre.
Madara's Ghost
3

2019:

Utilisez ce module natif const {promisify} = require('util'); pour convertir un ancien modèle de rappel en modèle de promesse afin que vous puissiez obtenir des avantages à partir du async/awaitcode

const {promisify} = require('util');
const glob = promisify(require('glob'));

app.get('/', async function (req, res) {
    const files = await glob('src/**/*-spec.js');
    res.render('mocha-template-test', {files});
});
pery mimon
la source
2

Lors de la mise en place d'une promesse, vous prenez deux paramètres, resolveet reject. En cas de succès, appelez resolveavec le résultat, en cas d'échec, appelezreject avec l'erreur.

Ensuite, vous pouvez écrire:

getUsers().then(callback)

callbacksera appelé avec le résultat de la promesse renvoyée getUsers, ieresult

À M
la source
2

En utilisant la bibliothèque Q par exemple:

function getUsers(param){
    var d = Q.defer();

    connection.connect(function () {
    connection.query('SELECT * FROM Users', function (err, result) {
        if(!err){
            d.resolve(result);
        }
    });
    });
    return d.promise;   
}
sacoche
la source
1
Serait, sinon {d.reject (nouvelle erreur (err)); },répare ça?
Russell
0

Le code ci-dessous ne fonctionne que pour le nœud -v> 8.x

J'utilise ce middleware MySQL promis pour Node.js

lire cet article Créer un middleware de base de données MySQL avec Node.js 8 et Async / Await

database.js

var mysql = require('mysql'); 

// node -v must > 8.x 
var util = require('util');


//  !!!!! for node version < 8.x only  !!!!!
// npm install util.promisify
//require('util.promisify').shim();
// -v < 8.x  has problem with async await so upgrade -v to v9.6.1 for this to work. 



// connection pool https://github.com/mysqljs/mysql   [1]
var pool = mysql.createPool({
  connectionLimit : process.env.mysql_connection_pool_Limit, // default:10
  host     : process.env.mysql_host,
  user     : process.env.mysql_user,
  password : process.env.mysql_password,
  database : process.env.mysql_database
})


// Ping database to check for common exception errors.
pool.getConnection((err, connection) => {
if (err) {
    if (err.code === 'PROTOCOL_CONNECTION_LOST') {
        console.error('Database connection was closed.')
    }
    if (err.code === 'ER_CON_COUNT_ERROR') {
        console.error('Database has too many connections.')
    }
    if (err.code === 'ECONNREFUSED') {
        console.error('Database connection was refused.')
    }
}

if (connection) connection.release()

 return
 })

// Promisify for Node.js async/await.
 pool.query = util.promisify(pool.query)



 module.exports = pool

Vous devez mettre à niveau le nœud -v> 8.x

vous devez utiliser la fonction async pour pouvoir utiliser await.

exemple:

   var pool = require('./database')

  // node -v must > 8.x, --> async / await  
  router.get('/:template', async function(req, res, next) 
  {
      ...
    try {
         var _sql_rest_url = 'SELECT * FROM arcgis_viewer.rest_url WHERE id='+ _url_id;
         var rows = await pool.query(_sql_rest_url)

         _url  = rows[0].rest_url // first record, property name is 'rest_url'
         if (_center_lat   == null) {_center_lat = rows[0].center_lat  }
         if (_center_long  == null) {_center_long= rows[0].center_long }
         if (_center_zoom  == null) {_center_zoom= rows[0].center_zoom }          
         _place = rows[0].place


       } catch(err) {
                        throw new Error(err)
       }
hoogw
la source