Zastępowanie wywołań zwrotnych obietnicami w Node.js.


94

Mam prosty moduł węzła, który łączy się z bazą danych i ma kilka funkcji do odbierania danych, na przykład ta funkcja:


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

Moduł zostałby nazwany w ten sposób z innego modułu węzła:


app.js:

import dbCon from './dbConnection.js';

dbCon.getUsers(console.log);

Chciałbym użyć obietnic zamiast wywołań zwrotnych, aby zwrócić dane. Do tej pory przeczytałem o zagnieżdżonych obietnicach w następującym wątku: Pisanie czystego kodu z zagnieżdżonymi obietnicami , ale nie mogłem znaleźć żadnego rozwiązania, które byłoby wystarczająco proste dla tego przypadku użycia. Jaki byłby właściwy sposób powrotu resultprzy użyciu obietnicy?


1
Zobacz Adapting Node , jeśli używasz biblioteki Q kriskowala.
Bertrand Marron

1
możliwy duplikat Jak przekonwertować istniejący interfejs API wywołania zwrotnego na obietnice? Proszę, doprecyzuj swoje pytanie, albo zamknę je
Bergi

@ leo.249: Czy przeczytałeś dokumentację Q? Czy próbowałeś już zastosować go do swojego kodu - jeśli tak, prześlij swoją próbę (nawet jeśli nie działa)? Gdzie dokładnie utkniesz? Wydaje się, że znalazłeś niełatwe rozwiązanie, prześlij je.
Bergi

3
@ leo.249 Q praktycznie nie jest konserwowane - ostatnie zatwierdzenie miało miejsce 3 miesiące temu. Tylko gałąź v2 jest interesująca dla programistów Q, a to i tak nie jest nawet bliskie gotowości do produkcji. W narzędziu do śledzenia problemów z października znajdują się nierozwiązane problemy bez komentarzy. Zdecydowanie sugeruję rozważenie dobrze utrzymanej biblioteki obietnic.
Benjamin Gruenbaum

Odpowiedzi:


103

Korzystanie z Promiseklasy

Polecam przyjrzeć się dokumentom MDN Promise, które stanowią dobry punkt wyjścia do korzystania z Promises. Alternatywnie, jestem pewien, że istnieje wiele tutoriali dostępnych online. :)

Uwaga: Nowoczesne przeglądarki obsługują już specyfikację Promises ECMAScript 6 (zobacz dokumentację MDN, do której link znajduje się powyżej) i zakładam, że chcesz użyć implementacji natywnej, bez bibliotek innych firm.

A jeśli chodzi o rzeczywisty przykład ...

Podstawowa zasada działa tak:

  1. Twój interfejs API nazywa się
  2. Tworzysz nowy obiekt Promise, ten obiekt przyjmuje pojedynczą funkcję jako parametr konstruktora
  3. Podana funkcja jest wywoływana przez podstawową implementację, a funkcja ma dwie funkcje - resolveireject
  4. Kiedy już wykonasz swoją logikę, wywołujesz jedną z nich, aby wypełnić obietnicę lub odrzucić ją z błędem

Może się wydawać, że to dużo, więc oto rzeczywisty przykład.

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

Korzystanie z funkcji języka async / await (Node.js> = 7.6)

W Node.js 7.6 kompilator JavaScript w wersji 8 został zaktualizowany z obsługą async / await . Możesz teraz zadeklarować funkcje jako istoty async, co oznacza, że ​​automatycznie zwracają a, Promisektóre jest rozwiązywane, gdy funkcja asynchroniczna zakończy wykonywanie. Wewnątrz tej funkcji możesz użyć awaitsłowa kluczowego, aby poczekać, aż rozwiąże się kolejna obietnica.

Oto przykład:

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
Obietnice są częścią specyfikacji ECMAScript 2015, a wersja 8 używana przez Node v0.12 zapewnia implementację tej części specyfikacji. Więc tak, nie są częścią rdzenia węzła - są częścią języka.
Robert Rossmann

1
Dobrze wiedzieć, miałem wrażenie, że aby korzystać z Promises, musisz zainstalować pakiet npm i użyć require (). Znalazłem pakiet obietnicy na npm, który implementuje Bones / styl A ++ i użyłem go, ale wciąż jestem nowy w samym węźle (nie JavaScript).
macguru2000

To mój ulubiony sposób pisania obietnic i asynchronicznego kodu architekta, głównie dlatego, że jest to spójny wzorzec, łatwy do odczytania i pozwala na wysoce ustrukturyzowany kod.

31

Z bluebirdem możesz użyć Promise.promisifyAll(i Promise.promisify), aby dodać gotowe metody Promise do dowolnego obiektu.

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

I użyj w ten sposób:

getUsersAsync().then(console.log);

lub

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

Dodawanie dystrybutorów

Bluebird obsługuje wiele funkcji, jedną z nich są dyspozytory, pozwala bezpiecznie pozbyć się połączenia po jego zakończeniu za pomocą Promise.usingi Promise.prototype.disposer. Oto przykład z mojej aplikacji:

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

Następnie użyj tego w ten sposób:

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

Spowoduje to automatyczne zakończenie połączenia, gdy obietnica zostanie rozwiązana z wartością (lub zostanie odrzucona za pomocą Error).


3
Doskonała odpowiedź, dzięki tobie korzystałem z bluebirda zamiast Q, dziękuję!
Lior Erez

2
Pamiętaj, że korzystając z obietnic, zgadzasz się używać ich try-catchprzy każdym połączeniu. Jeśli więc robisz to dość często, a Twój kod jest podobny do tego przykładowego, powinieneś to przemyśleć.
Andrey Popov

14

Node.js w wersji 8.0.0+:

Nie musisz już używać bluebird do obiecywania metod API węzła. Ponieważ od wersji 8+ możesz używać natywnego pliku 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')
        });
};

Teraz nie musisz używać żadnej biblioteki innej firmy, aby wykonać obietnicę.


3

Zakładając, że interfejs API adaptera bazy danych nie wyświetla Promisessię, możesz zrobić coś takiego:

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

Jeśli API bazy danych obsługuje, Promisesmożesz zrobić coś takiego: (tutaj widzisz moc Obietnic, twój plik wywołania zwrotnego prawie znika)

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

Używanie .then()do zwrócenia nowej (zagnieżdżonej) obietnicy.

Zadzwoń z:

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

Użyłem makiety API dla moich obietnic, twoje API może być inne. Jeśli pokażesz mi swoje API, mogę je dostosować.


2
Jaka biblioteka obietnic ma Promisekonstruktora i .promise()metodę?
Bergi

Dziękuję Ci. Po prostu ćwiczę kilka node.js i to, co zamieściłem, to wszystko, co do niego zostało, bardzo prosty przykład, aby dowiedzieć się, jak używać obietnic. Twoje rozwiązanie wygląda dobrze, ale jaki pakiet npm musiałbym zainstalować, aby z niego korzystać promise = new Promise();?
Lior Erez

Chociaż twoje API zwraca teraz Obietnicę, nie pozbyłeś się piramidy zagłady ani nie zrobiłeś przykładu, jak działają obietnice, aby zastąpić wywołania zwrotne.
Madara's Ghost

@ leo.249 Nie wiem, każda biblioteka Promise, która jest zgodna z Promises / A +, powinna być dobra. Zobacz: promisesaplus.com/@Bergi to nieistotne. @SecondRikudo, jeśli interfejs API, z którym się łączysz, nie obsługuje, Promisesto utkniesz z używaniem wywołań zwrotnych. Po wejściu na obiecane terytorium „piramida” znika. Zobacz drugi przykład kodu, aby zobaczyć, jak to działa.
Halcyon

@Halcyon Zobacz moją odpowiedź. Nawet istniejący interfejs API, który korzysta z wywołań zwrotnych, może zostać „obiecany” do gotowego interfejsu API Promise, co daje w rezultacie znacznie czystszy kod.
Madara's Ghost

3

2019:

Użyj tego modułu natywnego, const {promisify} = require('util');aby przekształcić zwykły stary wzorzec wywołania zwrotnego do wzorca obietnicy, aby uzyskać korzyści z async/awaitkodu

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

Konfigurując obietnicę, bierzesz dwa parametry resolvei reject. W przypadku powodzenia wywołaj resolvez wynikiem, w przypadku niepowodzenia wywołaj rejectz błędem.

Następnie możesz napisać:

getUsers().then(callback)

callbackzostanie wywołana z wynikiem zwróconej obietnicy getUsers, tjresult


2

Na przykład przy użyciu biblioteki 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
W przeciwnym razie {d.reject (nowy błąd (błąd)); },napraw to?
Russell

0

Poniższy kod działa tylko dla węzła -v> 8.x

Używam tego Promisified MySQL dla Node.js.

przeczytaj ten artykuł Tworzenie oprogramowania pośredniczącego bazy danych MySQL za pomocą Node.js 8 i 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

Musisz zaktualizować węzeł -v> 8.x

musisz użyć funkcji async, aby móc używać await.

przykład:

   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)
       }
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.