用 Node.js 中的承诺替换回调

我有一个简单的节点模块,它连接到一个数据库,并有几个函数来接收数据,例如这个函数:


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

为了返回数据,我想使用承诺而不是回调。 到目前为止,我已经在下面的线程中读到了关于嵌套承诺的内容: 使用嵌套承诺编写干净代码,但是我找不到任何对于这个用例来说足够简单的解决方案。 使用承诺返回 result的正确方法是什么?

96715 次浏览

When setting up a promise you take two parameters, resolve and reject. In the case of success, call resolve with the result, in the case of failure call reject with the error.

Then you can write:

getUsers().then(callback)

callback will be called with the result of the promise returned from getUsers, i.e. result

Assuming your database adapter API doesn't output Promises itself you can do something like:

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

If the database API does support Promises you could do something like: (here you see the power of Promises, your callback fluff pretty much disappears)

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

Using .then() to return a new (nested) promise.

Call with:

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

I used a mockup API for my Promises, your API might be different. If you show me your API I can tailor it.

Using the Q library for example:

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

With bluebird you can use Promise.promisifyAll (and Promise.promisify) to add Promise ready methods to any object.

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

And use like this:

getUsersAsync().then(console.log);

or

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

Adding disposers

Bluebird supports a lot of features, one of them is disposers, it allows you to safely dispose of a connection after it ended with the help of Promise.using and Promise.prototype.disposer. Here's an example from my 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();
});
}

Then use it like this:

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

This will automatically end the connection once the promise resolves with the value (or rejects with an Error).

Using the Promise class

I recommend to take a look at MDN's Promise docs which offer a good starting point for using Promises. Alternatively, I am sure there are many tutorials available online.:)

Note: Modern browsers already support ECMAScript 6 specification of Promises (see the MDN docs linked above) and I assume that you want to use the native implementation, without 3rd party libraries.

As for an actual example...

The basic principle works like this:

  1. Your API is called
  2. You create a new Promise object, this object takes a single function as constructor parameter
  3. Your provided function is called by the underlying implementation and the function is given two functions - resolve and reject
  4. Once you do your logic, you call one of these to either fullfill the Promise or reject it with an error

This might seem like a lot so here is an actual example.

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

Using the async/await language feature (Node.js >=7.6)

In Node.js 7.6, the v8 JavaScript compiler was upgraded with async/await support. You can now declare functions as being async, which means they automatically return a Promise which is resolved when the async function completes execution. Inside this function, you can use the await keyword to wait until another Promise resolves.

Here is an example:

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
}

Node.js version 8.0.0+:

You don't have to use bluebird to promisify the node API methods anymore. Because, from version 8+ you can use 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')
});
};

Now, don't have to use any 3rd party lib to do the promisify.

Below code works only for node -v > 8.x

I use this Promisified MySQL middleware for Node.js

read this article Create a MySQL Database Middleware with Node.js 8 and 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

You must upgrade node -v > 8.x

you must use async function to be able to use await.

example:

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

2019:

Use that native module const {promisify} = require('util'); to conver plain old callback pattern to promise pattern so you can get benfit from async/await code

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