Substituindo callbacks por promessas em Node.js


94

Eu tenho um módulo de nó simples que se conecta a um banco de dados e tem várias funções para receber dados, por exemplo esta função:


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

O módulo seria chamado desta forma a partir de um módulo de nó diferente:


app.js:

import dbCon from './dbConnection.js';

dbCon.getUsers(console.log);

Eu gostaria de usar promessas em vez de retornos de chamada para retornar os dados. Até agora, li sobre promessas aninhadas no seguinte thread: Escrevendo código limpo com promessas aninhadas , mas não consegui encontrar nenhuma solução que seja simples o suficiente para este caso de uso. Qual seria a maneira correta de retornar resultusando uma promessa?


1
Veja Adaptando o Nó , se você estiver usando a biblioteca Q de kriskowal.
Bertrand Marron


@ leo.249: Você leu a documentação Q? Você já tentou aplicá-lo ao seu código - se sim, poste sua tentativa (mesmo se não estiver funcionando)? Onde exatamente você está preso? Parece que você encontrou uma solução não simples, por favor poste.
Bergi

3
@ leo.249 Q está praticamente sem manutenção - o último commit foi há 3 meses. Apenas o branch v2 é interessante para os desenvolvedores Q e isso não está nem perto de estar pronto para produção de qualquer maneira. Existem problemas não resolvidos sem comentários no rastreador de problemas de outubro. Eu sugiro fortemente que você considere uma biblioteca de promessas bem mantida.
Benjamin Gruenbaum

Respostas:


102

Usando a Promiseclasse

Recomendo dar uma olhada nos documentos do Promise do MDN, que oferecem um bom ponto de partida para usar o Promises. Como alternativa, tenho certeza de que existem muitos tutoriais disponíveis online. :)

Observação: os navegadores modernos já oferecem suporte à especificação ECMAScript 6 do Promises (consulte os documentos MDN vinculados acima) e presumo que você deseja usar a implementação nativa, sem bibliotecas de terceiros.

Como um exemplo real ...

O princípio básico funciona assim:

  1. Sua API é chamada
  2. Você cria um novo objeto Promise, este objeto assume uma única função como parâmetro do construtor
  3. Sua função fornecida é chamada pela implementação subjacente e a função recebe duas funções - resolveereject
  4. Depois de fazer sua lógica, você chama um deles para cumprir a promessa ou rejeitá-la com um erro

Isso pode parecer muito, então aqui está um exemplo 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 o recurso de linguagem async / await (Node.js> = 7.6)

No Node.js 7.6, o compilador JavaScript v8 foi atualizado com suporte async / await . Agora você pode declarar funções como sendo async, o que significa que elas retornam automaticamente um Promiseque é resolvido quando a função assíncrona completa a execução. Dentro desta função, você pode usar a awaitpalavra-chave para esperar até que outra promessa seja resolvida.

Aqui está um exemplo:

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
As promessas fazem parte da especificação ECMAScript 2015 e a v8 usada pelo Node v0.12 fornece a implementação desta parte da especificação. Então, sim, eles não fazem parte do núcleo do Node - eles fazem parte da linguagem.
Robert Rossmann

1
Bom saber, tive a impressão de que, para usar o Promises, seria necessário instalar um pacote npm e usar require (). Eu encontrei o pacote de promessa no npm que implementa o estilo bare bones / A ++ e usei isso, mas ainda sou novo no próprio nó (não JavaScript).
macguru2000

Esta é minha maneira favorita de escrever promessas e arquitetar código assíncrono, principalmente porque é um padrão consistente, de fácil leitura e permite código altamente estruturado.

31

Com o bluebird você pode usar Promise.promisifyAll(e Promise.promisify) para adicionar métodos prontos para Promise a qualquer 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')
        });
};

E use assim:

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

Adicionando trituradores

O Bluebird oferece suporte a vários recursos, um deles são os trituradores, que permitem que você descarte uma conexão com segurança após o término com a ajuda de Promise.usinge Promise.prototype.disposer. Aqui está um exemplo do meu aplicativo:

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

Em seguida, use-o assim:

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

Isso encerrará automaticamente a conexão assim que a promessa for resolvida com o valor (ou rejeitada com um Error).


3
Excelente resposta, acabei usando o bluebird em vez do Q graças a você, obrigado!
Lior Erez

2
Lembre-se de usar promessas que você concorda em usar try-catchem cada chamada. Portanto, se você faz isso com frequência e a complexidade do código é semelhante à do exemplo, você deve reconsiderar isso.
Andrey Popov

14

Node.js versão 8.0.0+:

Você não precisa mais usar o bluebird para promisificar os métodos da API do nó. Porque a partir da versão 8+ você pode usar 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')
        });
};

Agora, não precisa usar nenhuma biblioteca de terceiros para fazer a promessa.


3

Supondo que a API do adaptador de banco de dados não produza Promisessozinha, você pode fazer 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();
};

Se a API do banco de dados oferecer suporte, Promisesvocê pode fazer algo como: (aqui você vê o poder das promessas, seu erro de retorno de chamada praticamente desaparece)

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

Usando .then()para retornar uma nova promessa (aninhada).

Ligue com:

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

Usei uma API de maquete para minhas promessas, sua API pode ser diferente. Se você me mostrar sua API, posso personalizá-la.


2
Que biblioteca de promessa tem um Promiseconstrutor e um .promise()método?
Bergi

Obrigado. Estou apenas praticando o node.js e o que postei foi tudo, um exemplo muito simples para descobrir como usar promessas. Sua solução parece boa, mas qual pacote npm eu teria que instalar para usar promise = new Promise();?
Lior Erez de

Embora sua API agora retorne uma promessa, você não se livrou da pirâmide da desgraça nem deu um exemplo de como as promessas funcionam para substituir os retornos de chamada.
Madara's Ghost de

@ leo.249 Não sei, qualquer biblioteca Promise que seja compatível com Promises / A + deve ser boa. Veja: promisesaplus.com/@Bergi é irrelevante. @SecondRikudo se a API com a qual você está fazendo interface não for compatível, Promisesentão você está preso ao uso de callbacks. Uma vez que você entra no território prometido, a 'pirâmide' desaparece. Veja o segundo exemplo de código sobre como isso funcionaria.
Halcyon

@Halcyon Veja minha resposta. Mesmo uma API existente que usa retornos de chamada pode ser "prometida" em uma API pronta para Promise, o que resulta em um código muito mais limpo.
Madara's Ghost de

3

2019:

Use esse módulo nativo const {promisify} = require('util');para converter o antigo padrão de retorno de chamada em um padrão de promessa para que você possa obter benefícios do 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

Ao configurar uma promessa, você usa dois parâmetros, resolvee reject. Em caso de sucesso, ligue resolvecom o resultado, em caso de falha ligue rejectcom o erro.

Então você pode escrever:

getUsers().then(callback)

callbackserá chamado com o resultado da promessa devolvida getUsers, ou seja,result


2

Usando a biblioteca Q, por exemplo:

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
Seria, senão {d.reject (new Error (err)); },Conserte isso?
Russell

0

O código abaixo funciona apenas para o nó -v> 8.x

Eu uso este middleware MySQL Promisified para Node.js

leia este artigo Criar um middleware de banco de dados MySQL com 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

Você deve atualizar o nó -v> 8.x

você deve usar a função assíncrona para poder usar o await.

exemplo:

   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)
       }
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.