用Node.js中的promise替换回调


94

我有一个简单的节点模块,该模块连接到数据库,并具有几个接收数据的功能,例如此功能:


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

该模块将从另一个节点模块中以这种方式调用:


app.js:

import dbCon from './dbConnection.js';

dbCon.getUsers(console.log);

我想使用promise而不是回调以返回数据。到目前为止,我已经在以下线程中阅读了有关嵌套promise的内容:使用嵌套的Promises编写干净的代码,但是我找不到适合该用例的任何简单解决方案。result用承诺退货的正确方法是什么?


1
如果您使用的是kriskowal的Q库,请参阅Adapting Node
贝特朗·马龙2015年

1
如何将现有的回调API转换为Promise的可能重复项请让您的问题更具体,否则我将结束它
Bergi 2015年

@ leo.249:您阅读了Q文档吗?您是否已尝试将其应用于代码-如果是,请发布您的尝试(即使不起作用)?你到底困在哪里?您似乎找到了一个非简单的解决方案,请发布它。
Bergi 2015年

3
@ leo.249 Q几乎得不到维护-上一次提交是3个月前。Q开发人员只对v2分支感兴趣,而且无论如何它都还无法投入生产。从10月开始,问题跟踪器中有未解决的问题,没有评论。我强烈建议您考虑一个维护良好的Promise库。
本杰明·格伦鲍姆

Answers:


102

使用Promise课程

我建议看一下MDN的Promise文档,它们为使用Promises提供了一个很好的起点。另外,我确定在线上有很多教程。:)

注意:现代浏览器已经支持Promises的ECMAScript 6规范(请参阅上面的MDN文档),并且我假设您要使用本机实现,而没有第三方库。

至于一个实际的例子...

基本原理如下:

  1. 您的API被称为
  2. 您创建一个新的Promise对象,该对象将单个函数用作构造函数参数
  3. 您提供的函数由基础实现调用,并且该函数具有两个函数- resolvereject
  4. 一旦您执行了逻辑,就可以调用其中之一来完全兑现“承诺”,或者拒绝它并返回错误

这看起来可能很多,所以这里是一个实际示例。

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

使用异步/等待语言功能(Node.js> = 7.6)

在Node.js 7.6中,v8 JavaScript编译器已通过async / await支持进行了升级。现在,您可以将函数声明为is async,这意味着它们会Promise在异步函数完成执行时自动返回一个已解析的。在此函数内,您可以使用await关键字等待另一个Promise解析。

这是一个例子:

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
承诺是ECMAScript 2015规范的一部分,Node v0.12使用的v8提供了该部分规范的实现。是的,它们不是Node核心的一部分-它们是语言的一部分。
罗伯特·罗斯曼

1
很高兴知道,我的印象是要使用Promises,您需要安装一个npm软件包并使用require()。我在npm上找到了Promise包,该包实现了裸露的骨骼/ A ++风格,并使用了它,但是它对节点本身还是一个新手(不是JavaScript)。
macguru2000

这是我编写Promise和架构师异步代码的最喜欢的方法,主要是因为它是一致的模式,易于阅读,并且允许高度结构化的代码。

31

使用bluebird,您可以使用Promise.promisifyAll(和Promise.promisify)将Promise ready方法添加到任何对象。

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

像这样使用:

getUsersAsync().then(console.log);

要么

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

添加处置器

Bluebird支持许多功能,其中之一是处理程序,通过Promise.usingand 结束连接后,它可以让您安全地处理连接Promise.prototype.disposer。这是我的应用程序中的一个示例:

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

然后像这样使用它:

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

一旦promise用值解析(或使用拒绝Error),这将自动终止连接。


3
很好的答案,我最终使用bluebird而不是Q谢谢你,谢谢!
Lior Erez 2015年

2
请记住,使用诺言表示您同意try-catch在每次通话中使用。因此,如果您经常这样做,并且代码复杂度与示例类似,则应重新考虑这一点。
Andrey Popov 2015年

14

Node.js版本8.0.0+:

您不必再使用bluebird来推广节点API方法。因为从版本8开始,您可以使用本机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')
        });
};

现在,不必使用任何第三方库进行承诺。


3

假设您的数据库适配器API不输出Promises自身,则可以执行以下操作:

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

如果数据库API确实支持Promises,则可以执行以下操作:(在这里您看到Promises的强大功能,您的回调绒毛几乎消失了)

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

使用.then()返回一个新的(嵌套的)承诺。

致电:

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

我为Promises使用了样机API,您的API可能有所不同。如果您向我展示您的API,我可以对其进行定制。


2
什么Promise库有Promise构造函数和.promise()方法?
Bergi 2015年

谢谢。我只是在练习一些node.js,而我发布的只是它的全部内容,这是一个非常简单的示例,用于弄清楚如何使用Promise。您的解决方案看起来不错,但是要使用它我必须安装哪些npm软件包promise = new Promise();
Lior Erez 2015年

当您的API现在返回一个Promise时,您还没有摆脱厄运金字塔,也没有做出一个如何用诺言取代回调的示例。
马达拉的幽灵2015年

@ leo.249我不知道,任何与Promises / A +兼容的Promise库都应该不错。请参阅:promisesaplus.com/@Bergi,这无关紧要。@SecondRikudo如果您所连接的API不支持,Promises那么您将无法使用回调。一旦进入诺言领域,“金字塔”就会消失。请参阅第二个代码示例,以了解如何工作。
Halcyon 2015年

@Halcyon看看我的回答。即使是使用回调的现有API,也可以“承诺化”为Promise就绪的API,从而使代码更加整洁。
马达拉的幽灵2015年

3

2019年

使用该本机模块const {promisify} = require('util');将普通的旧回调模式转换为Promise模式,这样您就可以从async/await代码中受益

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

当建立一个承诺,你需要两个参数,resolvereject。如果成功,则调用resolve结果,如果失败,则调用reject错误。

然后您可以编写:

getUsers().then(callback)

callback将以的返回结果作为调用getUsers,即result


2

以Q库为例:

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
否则会是{d.reject(new Error(err)); },解决这个问题?
罗素

0

以下代码仅适用于节点-v> 8.x

我将这个承诺的MySQL中间件用于Node.js

阅读本文,使用Node.js 8和Async / Await创建MySQL数据库中间件

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

您必须升级节点-v> 8.x

您必须使用异步功能才能使用等待。

例:

   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)
       }
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.