Заміна зворотних дзвінків обіцянками в 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 місяці тому. Тільки галузь v2 цікава розробникам Q, і це навіть не готове до виробництва. У трекері випусків з жовтня є нерозглянуті питання без коментарів. Я настійно пропоную вам розглянути доглянуту бібліотеку з обіцянками.
Бенджамін Грюнбаум

Відповіді:


102

Використання Promiseкласу

Рекомендую ознайомитись з документами MDN's Promise, які пропонують хорошу відправну точку для використання обіцянок. Крім того, я впевнений, що в Інтернеті є багато навчальних посібників. :)

Примітка: Сучасні браузери вже підтримують специфікацію Обіцянок 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
  })

Використання функції мови асинхронізації / очікування (Node.js> = 7.6)

У Node.js 7.6 компілятор v8 JavaScript був оновлений підтримкою async / wait . Тепер ви можете оголосити функції як такі async, що означає, що вони автоматично повертають рішення, Promiseяке вирішується, коли функція асинхронізації завершує виконання. Всередині цієї функції ви можете використовувати 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, а v8, що використовується Node v0.12, забезпечує реалізацію цієї частини специфікації. Так, так, вони не є частиною ядра Node - вони є частиною мови.
Роберт Россманн

1
Приємно знати, у мене склалося враження, що для використання Promises вам потрібно буде встановити пакет npm та користуватися requ (). Я знайшов пакет з обіцянками в npm, який реалізує стилі голі кістки / A ++ і використовував це, але я все ще новий в самому вузлі (не JavaScript).
macguru2000

Це мій улюблений спосіб писати обіцянки та асинхронний код архітектора, головним чином тому, що це послідовний зразок, легко читається та дозволяє високоструктурований код.

31

За допомогою bluebird ви можете використовувати Promise.promisifyAllPromise.promisify), щоб додати готові методи Обіцяти до будь-якого об’єкта.

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
Відмінна відповідь, я в кінцевому підсумку використав синій птах замість Q завдяки вам, дякую!
Ліор Ерез

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

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ви можете зробити щось на кшталт: (тут ви бачите силу Обіцянь, ваш зворотний виклик майже зникає)

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 Дивіться мою відповідь. Навіть існуючий API, який використовує зворотні дзвінки, може бути "переосмислений" в API, готовий до обіцянки, що в цілому приводить до більш чистого коду.
Привид

3

2019:

Використовуйте цей власний модуль const {promisify} = require('util');для перетворення простого старого шаблону зворотного виклику в шаблон, який обіцяє, щоб ви могли отримати benfit з 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 (нова помилка (помилка)); }, виправити це?
Рассел

0

Нижче код працює лише для вузла -v> 8.x

Я використовую цю рекламну проміжну програму 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

ви повинні використовувати функцію async, щоб мати можливість очікувати.

приклад:

   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.