Sostituzione di callback con promesse in Node.js


94

Ho un semplice modulo nodo che si collega a un database e ha diverse funzioni per ricevere dati, ad esempio questa funzione:


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

Il modulo verrebbe chiamato in questo modo da un diverso modulo del nodo:


app.js:

import dbCon from './dbConnection.js';

dbCon.getUsers(console.log);

Vorrei utilizzare promesse invece di callback per restituire i dati. Finora ho letto di promesse annidate nel seguente thread: Scrittura di codice pulito con promesse annidate , ma non sono riuscito a trovare alcuna soluzione abbastanza semplice per questo caso d'uso. Quale sarebbe il modo corretto per tornare resultutilizzando una promessa?


1
Vedi Adapting Node , se stai usando la libreria Q di kriskowal.
Bertrand Marron

1
possibile duplicato di Come si converte un'API di callback esistente in promesse? Per favore, rendi la tua domanda più specifica, o la
chiuderò

@ leo.249: Hai letto la documentazione di Q? Hai già provato ad applicarlo al tuo codice - se sì, per favore pubblica il tuo tentativo (anche se non funziona)? Dove sei bloccato esattamente? Sembra che tu abbia trovato una soluzione non semplice, per favore pubblicala.
Bergi

3
@ leo.249 Q è praticamente non mantenuto - l'ultimo commit è stato di 3 mesi fa. Solo il ramo v2 è interessante per gli sviluppatori Q e comunque non è nemmeno vicino all'essere pronto per la produzione. Ci sono problemi non risolti senza commenti nel tracker dei problemi da ottobre. Suggerisco caldamente di considerare una libreria di promesse ben mantenuta.
Benjamin Gruenbaum

Risposte:


102

Usando la Promiseclasse

Consiglio di dare un'occhiata ai documenti Promise di MDN che offrono un buon punto di partenza per l'utilizzo di Promises. In alternativa, sono sicuro che ci siano molti tutorial disponibili online. :)

Nota: i browser moderni supportano già la specifica ECMAScript 6 di Promises (vedi i documenti MDN collegati sopra) e presumo che tu voglia utilizzare l'implementazione nativa, senza librerie di terze parti.

Per quanto riguarda un esempio reale ...

Il principio di base funziona in questo modo:

  1. La tua API viene chiamata
  2. Crei un nuovo oggetto Promise, questo oggetto accetta una singola funzione come parametro del costruttore
  3. La funzione fornita viene chiamata dall'implementazione sottostante e alla funzione vengono assegnate due funzioni: resolveereject
  4. Una volta che hai fatto la tua logica, chiami uno di questi per adempiere alla Promessa o rifiutarla con un errore

Potrebbe sembrare molto, quindi ecco un esempio reale.

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

Utilizzo della funzione di lingua asincrona / attesa (Node.js> = 7.6)

In Node.js 7.6, il compilatore JavaScript v8 è stato aggiornato con il supporto async / await . È ora possibile dichiarare le funzioni come essere async, il che significa che restituiscono automaticamente un Promiseche viene risolto quando la funzione asincrona completa l'esecuzione. All'interno di questa funzione, puoi utilizzare la awaitparola chiave per attendere che un'altra promessa si risolva.

Ecco un esempio:

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
Le promesse fanno parte della specifica ECMAScript 2015 e la v8 utilizzata da Node v0.12 fornisce l'implementazione di questa parte della specifica. Quindi sì, non fanno parte del nucleo di Node - fanno parte del linguaggio.
Robert Rossmann

1
Buono a sapersi, avevo l'impressione che per usare Promises avresti bisogno di installare un pacchetto npm e usare require (). Ho trovato il pacchetto promise su npm che implementa lo stile bare Bone / A ++ e l'ho usato, ma sono ancora nuovo per il nodo stesso (non JavaScript).
macguru2000

Questo è il mio modo preferito per scrivere promesse e progettare codice asincrono, principalmente perché è un modello coerente, di facile lettura e consente un codice altamente strutturato.

31

Con bluebird puoi usare Promise.promisifyAll(e Promise.promisify) per aggiungere metodi pronti per Promise a qualsiasi oggetto.

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

E usa in questo modo:

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

Aggiunta di dissipatori

Bluebird supporta molte funzionalità, una di queste è disposers, ti consente di smaltire in sicurezza una connessione dopo che è terminata con l'aiuto di Promise.usinge Promise.prototype.disposer. Ecco un esempio dalla mia app:

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

Quindi usalo in questo modo:

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

Ciò terminerà automaticamente la connessione una volta che la promessa si risolve con il valore (o rifiuta con un Error).


3
Ottima risposta, ho finito per usare bluebird invece di Q grazie a te, grazie!
Lior Erez

2
Tieni presente che utilizzando le promesse che accetti di utilizzare try-catchin ogni chiamata. Quindi, se lo fai abbastanza spesso e la complessità del tuo codice è simile all'esempio, dovresti riconsiderarlo.
Andrey Popov,

14

Node.js versione 8.0.0+:

Non è più necessario utilizzare bluebird per promettere i metodi dell'API del nodo. Perché, dalla versione 8+ puoi usare native 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')
        });
};

Ora, non è necessario utilizzare alcuna libreria di terze parti per eseguire la promessa.


3

Supponendo che l'API dell'adattatore del database non venga visualizzato da Promisessolo, puoi fare qualcosa come:

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

Se l'API del database supporta, Promisespotresti fare qualcosa del tipo: (qui vedi il potere di Promises, il tuo fluff di callback praticamente scompare)

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

Utilizzo .then()per restituire una nuova promessa (annidata).

Chiama con:

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

Ho usato un'API mockup per le mie promesse, la tua API potrebbe essere diversa. Se mi mostri la tua API posso adattarla.


2
Quale libreria di promesse ha un Promisecostruttore e un .promise()metodo?
Bergi

Grazie. Sto solo praticando un po 'di node.js e quello che ho pubblicato era tutto ciò che c'era da fare, un esempio molto semplice per capire come usare le promesse. La tua soluzione sembra buona, ma quale pacchetto npm dovrei installare per poterlo utilizzare promise = new Promise();?
Lior Erez

Sebbene la tua API ora restituisca una promessa, non ti sei sbarazzato della piramide del destino, né hai fatto un esempio di come funzionano le promesse per sostituire i callback.
Il fantasma di Madara il

@ leo.249 Non lo so, qualsiasi libreria Promise conforme a Promises / A + dovrebbe essere valida. Vedi: promisesaplus.com/@Bergi è irrilevante. @SecondRikudo se l'API con cui ti stai interfacciando non supporta Promises, sei bloccato con l'utilizzo dei callback. Una volta entrati nel territorio delle promesse, la "piramide" scompare. Vedi il secondo esempio di codice su come funzionerebbe.
Halcyon

@Halcyon Vedi la mia risposta. Anche un'API esistente che utilizza i callback può essere "promessa" in un'API pronta per Promise, il che si traduce in un codice molto più pulito.
Il fantasma di Madara il

3

2019:

Usa quel modulo nativo const {promisify} = require('util');per convertire il vecchio modello di callback in un modello di promessa in modo da poter trarre vantaggio dal async/awaitcodice

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

Quando si imposta una promessa si prendono due parametri, resolvee reject. In caso di successo chiamare resolvecon il risultato, in caso di fallimento chiamare rejectcon l'errore.

Quindi puoi scrivere:

getUsers().then(callback)

callbackverrà chiamato con il risultato della promessa restituita da getUsers, ieresult


2

Usando la libreria Q ad esempio:

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
Sì, altrimenti {d.reject (new Error (err)); }, risolverlo?
Russell

0

Il codice seguente funziona solo per il nodo -v> 8.x

Uso questo middleware MySQL promesso per Node.js

leggi questo articolo Crea un middleware per database MySQL con Node.js 8 e 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

È necessario aggiornare il nodo -v> 8.x

è necessario utilizzare la funzione asincrona per poter utilizzare await.

esempio:

   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)
       }
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.