Reemplazo de devoluciones de llamada con promesas en Node.js


94

Tengo un módulo de nodo simple que se conecta a una base de datos y tiene varias funciones para recibir datos, por ejemplo esta función:


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

El módulo se llamaría de esta manera desde un módulo de nodo diferente:


app.js:

import dbCon from './dbConnection.js';

dbCon.getUsers(console.log);

Me gustaría usar promesas en lugar de devoluciones de llamada para devolver los datos. Hasta ahora he leído sobre las promesas anidadas en el siguiente hilo: Escribir código limpio con promesas anidadas , pero no pude encontrar ninguna solución que sea lo suficientemente simple para este caso de uso. ¿Cuál sería la forma correcta de regresar resultusando una promesa?


1
Consulte Adaptación de nodo , si está utilizando la biblioteca Q de kriskowal.
Bertrand Marron

1
posible duplicado de ¿Cómo convierto una API de devolución de llamada existente en promesas? Haga su pregunta más específica, o la
cerraré

@ leo.249: ¿Ha leído la documentación de Q? ¿Ha intentado ya aplicarlo a su código? En caso afirmativo, publique su intento (incluso si no funciona). ¿Dónde estás atrapado exactamente? Parece que ha encontrado una solución no sencilla, publíquela.
Bergi

3
@ leo.249 Q está prácticamente sin mantenimiento: la última confirmación fue hace 3 meses. Solo la rama v2 es interesante para los desarrolladores de Q y eso ni siquiera está cerca de estar listo para producción de todos modos. Hay problemas no resueltos sin comentarios en el rastreador de problemas de octubre. Le sugiero que considere una biblioteca de promesas bien mantenida.
Benjamin Gruenbaum

Respuestas:


102

Usando la Promiseclase

Recomiendo echar un vistazo a los documentos de Promise de MDN que ofrecen un buen punto de partida para usar Promises. Alternativamente, estoy seguro de que hay muchos tutoriales disponibles en línea. :)

Nota: Los navegadores modernos ya admiten la especificación ECMAScript 6 de Promises (consulte los documentos de MDN vinculados anteriormente) y supongo que desea usar la implementación nativa, sin bibliotecas de terceros.

En cuanto a un ejemplo real ...

El principio básico funciona así:

  1. Tu API se llama
  2. Crea un nuevo objeto Promise, este objeto toma una única función como parámetro de constructor
  3. Su función proporcionada es llamada por la implementación subyacente y la función recibe dos funciones: resolveyreject
  4. Una vez que haga su lógica, llame a uno de estos para cumplir la Promesa o rechazarla con un error

Esto puede parecer mucho, así que aquí hay un ejemplo real.

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

Usando la función de lenguaje asincrónico / en espera (Node.js> = 7.6)

En Node.js 7.6, el compilador de JavaScript v8 se actualizó con soporte async / await . Ahora puede declarar funciones como siendo async, lo que significa que devuelven automáticamente un Promiseque se resuelve cuando la función asíncrona completa la ejecución. Dentro de esta función, puede usar la awaitpalabra clave para esperar hasta que se resuelva otra Promesa.

Aquí hay un ejemplo:

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
}

14
Las promesas son parte de la especificación ECMAScript 2015 y la versión 8 utilizada por Node v0.12 proporciona la implementación de esta parte de la especificación. Entonces, sí, no son parte del núcleo de Node, son parte del lenguaje.
Robert Rossmann

1
Es bueno saberlo, tenía la impresión de que para usar Promises necesitaría instalar un paquete npm y usar require (). Encontré el paquete de promesa en npm que implementa el estilo básico / A ++ y lo he usado, pero todavía soy nuevo en el nodo en sí (no en JavaScript).
macguru2000

Esta es mi forma favorita de escribir promesas y diseñar código asincrónico, principalmente porque es un patrón consistente, fácil de leer y permite un código altamente estructurado.

31

Con bluebird puede usar Promise.promisifyAll(y Promise.promisify) para agregar métodos listos para Promise a cualquier objeto.

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

Y usa así:

getUsersAsync().then(console.log);

o

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

Agregar trituradores

Bluebird admite muchas funciones, una de ellas son los trituradores, que le permite eliminar de forma segura una conexión después de que finaliza con la ayuda de Promise.usingy Promise.prototype.disposer. Aquí hay un ejemplo de mi aplicación:

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

Entonces úsalo así:

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

Esto terminará automáticamente la conexión una vez que la promesa se resuelva con el valor (o se rechace con un Error).


3
Excelente respuesta, terminé usando bluebird en lugar de Q gracias a ti, ¡gracias!
Lior Erez

2
Tenga en cuenta que el uso de promesas que acepta usar try-catchen cada llamada. Entonces, si lo hace con bastante frecuencia, y la complejidad de su código es similar a la de la muestra, debería reconsiderar esto.
Andrey Popov

14

Node.js versión 8.0.0+:

Ya no tiene que usar bluebird para prometer los métodos de API de nodo. Porque, a partir de la versión 8+, puede utilizar util.promisify nativo :

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

Ahora, no tienes que usar ninguna biblioteca de terceros para hacer la promesa.


3

Suponiendo que la API de su adaptador de base de datos no se genera por Promisessí misma, puede hacer algo como:

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 la API de la base de datos es compatible Promises, puede hacer algo como: (aquí puede ver el poder de Promises, su error de devolución de llamada prácticamente desaparece)

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

Utilizando .then()para devolver una nueva promesa (anidada).

Llamar con:

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

Usé una API de maqueta para mis Promesas, su API puede ser diferente. Si me muestra su API, puedo adaptarla.


2
¿Qué biblioteca de promesas tiene un Promiseconstructor y un .promise()método?
Bergi

Gracias. Solo estoy practicando algunos node.js y lo que publiqué fue todo lo que hay que hacer, un ejemplo muy simple para descubrir cómo usar las promesas. Su solución se ve bien, pero ¿qué paquete npm tendría que instalar para usarlo promise = new Promise();?
Lior Erez

Si bien su API ahora devuelve una Promesa, no se ha deshecho de la pirámide de la fatalidad ni ha dado un ejemplo de cómo funcionan las promesas para reemplazar las devoluciones de llamada.
Madara's Ghost

@ leo.249 No lo sé, cualquier biblioteca de Promise que cumpla con Promises / A + debería ser buena. Ver: promisesaplus.com/@Bergi es irrelevante. @SecondRikudo si la API con la que está interactuando no es compatible, Promisesentonces está atascado con el uso de devoluciones de llamada. Una vez que entras en territorio prometido, la 'pirámide' desaparece. Vea el segundo ejemplo de código sobre cómo funcionaría.
Halcyon

@Halcyon Vea mi respuesta. Incluso una API existente que usa devoluciones de llamada puede "promisarse" en una API lista para Promise, lo que da como resultado un código mucho más limpio.
Madara's Ghost

3

2019:

Use ese módulo nativo const {promisify} = require('util');para convertir el patrón de devolución de llamada antiguo simple en el patrón de promesa para que pueda beneficiarse del async/awaitcódigo

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

2

Al configurar una promesa, toma dos parámetros resolvey reject. En caso de éxito, llame resolvecon el resultado, en caso de falla, llame rejectcon el error.

Entonces puedes escribir:

getUsers().then(callback)

callbackserá llamado con el resultado de la promesa devuelta getUsers, es decirresult


2

Usando la biblioteca Q, por ejemplo:

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

1
De lo contrario, {d.reject (nuevo Error (err)); },¿Arregla eso?
Russell

0

El siguiente código funciona solo para el nodo -v> 8.x

Utilizo este middleware MySQL promisificado para Node.js

lea este artículo Cree un middleware de base de datos MySQL con Node.js 8 y 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

Debe actualizar el nodo -v> 8.x

debe usar la función asíncrona para poder usar await.

ejemplo:

   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)
       }
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.