Замена обратных вызовов обещаниями в Node.js


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

Я хотел бы использовать обещания вместо обратных вызовов, чтобы возвращать данные. До сих пор я читал о вложенных обещаниях в следующем потоке: Написание чистого кода с вложенными обещаниями , но я не смог найти решения, которое было бы достаточно простым для этого варианта использования. Как правильно вернуться resultс помощью обещания?


1
См. Раздел « Адаптация узла» , если вы используете библиотеку Q от kriskowal.
Бертран Маррон,

1
возможный дубликат Как преобразовать существующий API обратного вызова в обещания? Пожалуйста, уточните свой вопрос, или я
закрою

@ leo.249: Вы читали документацию Q? Вы уже пытались применить это к своему коду - если да, опубликуйте свою попытку (даже если не работает)? Где именно ты застрял? Кажется, вы нашли непростое решение, опубликуйте его.
Берги

3
@ leo.249 Q практически не поддерживается - последняя фиксация была 3 месяца назад. Разработчикам Q интересна только ветка v2, которая в любом случае даже близко не готова к производству. В системе отслеживания ошибок с октября есть нерешенные проблемы без комментариев. Я настоятельно рекомендую вам подумать о хорошо поддерживаемой библиотеке обещаний.
Бенджамин Грюнбаум

Ответы:


103

Использование Promiseкласса

Я рекомендую взглянуть на документы MDN Promise, которые предлагают хорошую отправную точку для использования Promises. В качестве альтернативы, я уверен, что в Интернете есть много обучающих программ. :)

Примечание. Современные браузеры уже поддерживают спецификацию обещаний ECMAScript 6 (см. Приведенные выше документы MDN), и я предполагаю, что вы хотите использовать собственную реализацию без сторонних библиотек.

Что касается реального примера ...

Основной принцип работает так:

  1. Ваш API называется
  2. Вы создаете новый объект Promise, этот объект принимает одну функцию в качестве параметра конструктора.
  3. Ваша предоставленная функция вызывается базовой реализацией, и функции предоставляются две функции - resolveиreject
  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
  })

Использование функции языка async / await (Node.js> = 7.6)

В Node.js 7.6 компилятор JavaScript v8 был обновлен с поддержкой async / await . Теперь вы можете объявлять функции как существующие async, что означает, что они автоматически возвращают, Promiseкоторый разрешается, когда функция async завершает выполнение. Внутри этой функции вы можете использовать awaitключевое слово, чтобы дождаться разрешения другого обещания.

Вот пример:

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, а версия 8, используемая в Node v0.12, обеспечивает реализацию этой части спецификации. Так что да, они не являются частью ядра Node - они являются частью языка.
Роберт Россманн

1
Полезно знать, у меня создалось впечатление, что для использования Promises вам необходимо установить пакет npm и использовать require (). Я нашел пакет обещаний на npm, который реализует голый стиль / стиль A ++, и использовал его, но я все еще новичок в самом node (а не в JavaScript).
macguru2000

Это мой любимый способ писать обещания и создавать асинхронный код, главным образом потому, что это согласованный шаблон, легко читаемый и допускающий высокоструктурированный код.

31

С bluebird вы можете использовать Promise.promisifyAllPromise.promisify) для добавления методов Promise к любому объекту.

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

Это автоматически завершит соединение, как только обещание будет разрешено со значением (или отклонено с помощью Error).


3
Отличный ответ, я закончил использовать bluebird вместо Q благодаря вам, спасибо!
Лиор Эрез

2
Помните, что, используя обещания, вы соглашаетесь использовать их try-catchпри каждом звонке. Так что если вы делаете это довольно часто и сложность вашего кода аналогична образцу, вам следует пересмотреть это.
Андрей Попов

14

Node.js версии 8.0.0+:

Вы не должны использовать Блюберд больше promisify методы узла 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вы можете сделать что-то вроде: (здесь вы видите мощь обещаний, ваш бред обратного вызова в значительной степени исчезает)

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

Используется .then()для возврата нового (вложенного) обещания.

Звоните с:

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

Я использовал макет API для своих обещаний, ваш API может быть другим. Если вы покажете мне свой API, я смогу его адаптировать.


2
В какой библиотеке обещаний есть Promiseконструктор и .promise()метод?
Берги

Спасибо. Я просто практикуюсь в node.js, и то, что я опубликовал, было всем, что нужно, очень простой пример, чтобы выяснить, как использовать обещания. Ваше решение выглядит хорошо, но какой пакет npm мне нужно установить, чтобы использовать promise = new Promise();?
Лиор Эрез

Хотя ваш API теперь возвращает обещание, вы не избавились от пирамиды обреченности и не сделали примера того, как обещания работают для замены обратных вызовов.
Призрак Мадары

@ leo.249 Я не знаю, любая библиотека Promise, совместимая с Promises / A +, должна быть хорошей. Смотрите: promisesaplus.com/@Bergi, это не имеет значения. @SecondRikudo, если API, с которым вы взаимодействуете, не поддерживает, Promisesвы застряли в использовании обратных вызовов. Как только вы попадете на обещанную территорию, «пирамида» исчезнет. Посмотрите второй пример кода, чтобы узнать, как это будет работать.
Halcyon

@Halcyon Смотрите мой ответ. Даже существующий API, который использует обратные вызовы, может быть «обещан» в готовый к Promise API, что в целом приводит к гораздо более чистому коду.
Призрак Мадары

3

2019:

Используйте этот собственный модуль const {promisify} = require('util');для преобразования простого старого шаблона обратного вызова в шаблон обещания, чтобы вы могли получить выгоду от 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

При настройке обещания вы берете два параметра resolveи reject. В случае успеха вызов 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 (новая ошибка (err)); }, исправить?
Рассел

0

Код ниже работает только для узла -v> 8.x

Я использую это промежуточное ПО Promisified MySQL для Node.js

прочтите эту статью Создание промежуточного программного обеспечения базы данных MySQL с помощью Node.js 8 и 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

Вы должны обновить узел -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)
       }
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.