Thay thế các lệnh gọi lại bằng các lời hứa trong Node.js


94

Tôi có một mô-đun nút đơn giản kết nối với cơ sở dữ liệu và có một số chức năng để nhận dữ liệu, ví dụ như chức năng này:


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

Mô-đun sẽ được gọi theo cách này từ một mô-đun nút khác:


app.js:

import dbCon from './dbConnection.js';

dbCon.getUsers(console.log);

Tôi muốn sử dụng các lời hứa thay vì lệnh gọi lại để trả về dữ liệu. Cho đến nay tôi đã đọc về các lời hứa lồng nhau trong chủ đề sau: Viết mã sạch với lời hứa lồng nhau , nhưng tôi không thể tìm thấy bất kỳ giải pháp nào đủ đơn giản cho trường hợp sử dụng này. Cách chính xác để quay lại resultbằng lời hứa là gì?


1
Xem Nút thích ứng , nếu bạn đang sử dụng thư viện Q của kriskowal.
Bertrand Marron

1
có thể trùng lặp Làm cách nào để chuyển đổi API gọi lại hiện có thành các hứa hẹn? Vui lòng làm cho câu hỏi của bạn cụ thể hơn, hoặc tôi sẽ đóng nó
Bergi

@ leo.249: Bạn đã đọc tài liệu Q chưa? Bạn đã thử áp dụng nó cho mã của mình chưa - nếu có, vui lòng đăng thử của bạn (ngay cả khi không hoạt động)? Chính xác thì bạn đang mắc kẹt ở đâu? Bạn dường như đã tìm ra một giải pháp không hề đơn giản, hãy đăng nó lên.
Bergi

3
@ leo.249 Q thực tế là không rõ ràng - lần cam kết gần đây nhất là 3 tháng trước. Chỉ có nhánh v2 là thú vị đối với các nhà phát triển Q và dù sao thì điều đó thậm chí vẫn chưa sẵn sàng cho việc sản xuất. Có vấn đề chưa được giải quyết mà không có nhận xét trong trình theo dõi vấn đề từ tháng 10. Tôi thực sự khuyên bạn nên xem xét một thư viện lời hứa được duy trì tốt.
Benjamin Gruenbaum

Câu trả lời:


102

Sử dụng Promiselớp học

Tôi khuyên bạn nên xem qua các tài liệu về Promise của MDN, tài liệu này cung cấp một điểm khởi đầu tốt để sử dụng Promises. Ngoài ra, tôi chắc chắn rằng có rất nhiều hướng dẫn có sẵn trực tuyến. :)

Lưu ý: Các trình duyệt hiện đại đã hỗ trợ đặc tả ECMAScript 6 của Promises (xem tài liệu MDN được liên kết ở trên) và tôi giả sử rằng bạn muốn sử dụng triển khai gốc mà không cần thư viện bên thứ ba.

Đối với một ví dụ thực tế ...

Nguyên tắc cơ bản hoạt động như sau:

  1. API của bạn được gọi là
  2. Bạn tạo một đối tượng Promise mới, đối tượng này nhận một hàm duy nhất làm tham số khởi tạo
  3. Hàm đã cung cấp của bạn được gọi bằng cách triển khai cơ bản và hàm được cung cấp hai hàm - resolvereject
  4. Khi bạn thực hiện logic của mình, bạn gọi một trong số những thứ này để điền đầy đủ Lời hứa hoặc từ chối nó với lỗi

Điều này có vẻ giống rất nhiều vì vậy đây là một ví dụ thực tế.

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

Sử dụng tính năng ngôn ngữ async / await (Node.js> = 7.6)

Trong Node.js 7.6, trình biên dịch JavaScript v8 đã được nâng cấp với hỗ trợ async / await . Bây giờ bạn có thể khai báo các hàm như hiện tại async, có nghĩa là chúng tự động trả về một hàm Promiseđược giải quyết khi hàm không đồng bộ hoàn thành việc thực thi. Bên trong chức năng này, bạn có thể sử dụng awaittừ khóa để đợi cho đến khi một Lời hứa khác được giải quyết.

Đây là một ví dụ:

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
Hứa hẹn là một phần của đặc tả ECMAScript 2015 và v8 được sử dụng bởi Node v0.12 cung cấp việc triển khai phần này của thông số kỹ thuật. Vì vậy, có, chúng không phải là một phần của lõi Node - chúng là một phần của ngôn ngữ.
Robert Rossmann

1
Thật tốt khi biết, tôi có ấn tượng rằng để sử dụng Promises, bạn sẽ cần cài đặt gói npm và sử dụng request (). Tôi đã tìm thấy gói hứa hẹn trên npm triển khai phong cách bare bone / A ++ và đã sử dụng nó, nhưng vẫn còn mới đối với chính nút (không phải JavaScript).
macguru2000,

Đây là cách yêu thích của tôi để viết lời hứa và mã không đồng bộ kiến ​​trúc, chủ yếu vì nó là một mẫu nhất quán, dễ đọc và cho phép mã có cấu trúc cao.

31

Với bluebird, bạn có thể sử dụng Promise.promisifyAll(và Promise.promisify) để thêm các phương thức sẵn sàng của Promise vào bất kỳ đối tượng nào.

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

Và sử dụng như thế này:

getUsersAsync().then(console.log);

hoặc là

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

Thêm người định đoạt

Bluebird hỗ trợ rất nhiều tính năng, một trong số đó là tính năng hủy bỏ, nó cho phép bạn hủy kết nối một cách an toàn sau khi kết thúc với sự trợ giúp của Promise.usingPromise.prototype.disposer. Đây là một ví dụ từ ứng dụng của tôi:

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

Sau đó sử dụng nó như thế này:

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

Điều này sẽ tự động kết thúc kết nối khi lời hứa giải quyết bằng giá trị (hoặc từ chối bằng một Error).


3
Câu trả lời xuất sắc, tôi đã sử dụng bluebird thay vì Q cảm ơn bạn, cảm ơn bạn!
Lior Erez

2
Hãy nhớ rằng sử dụng những lời hứa mà bạn đồng ý sử dụng try-catchtrong mỗi cuộc gọi. Vì vậy, nếu bạn làm điều đó khá thường xuyên và độ phức tạp mã của bạn tương tự như mẫu, thì bạn nên xem xét lại điều này.
Andrey Popov

14

Phiên bản Node.js 8.0.0+:

Bạn không phải sử dụng bluebird để quảng bá các phương thức API nút nữa. Bởi vì, từ phiên bản 8+, bạn có thể sử dụng bản địa chỉ dùng riêng .

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

Bây giờ, không cần phải sử dụng bất kỳ lib của bên thứ 3 nào để thực hiện quảng cáo.


3

Giả sử API bộ điều hợp cơ sở dữ liệu của bạn không Promisestự xuất ra, bạn có thể làm điều gì đó như:

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

Nếu API cơ sở dữ liệu hỗ trợ, Promisesbạn có thể làm điều gì đó như: (ở đây bạn thấy sức mạnh của Lời hứa, lông tơ gọi lại của bạn biến mất khá nhiều)

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

Sử dụng .then()để trả về một lời hứa mới (lồng nhau).

Gọi bằng:

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

Tôi đã sử dụng một API giả lập cho Lời hứa của mình, API của bạn có thể khác. Nếu bạn cho tôi xem API của bạn, tôi có thể điều chỉnh nó.


2
Thư viện lời hứa nào có hàm Promisetạo và .promise()phương thức?
Bergi

Cảm ơn bạn. Tôi chỉ đang thực hành một số node.js và những gì tôi đã đăng là tất cả những gì có liên quan đến nó, ví dụ rất đơn giản để tìm ra cách sử dụng các hứa hẹn. Giải pháp của bạn có vẻ tốt nhưng tôi sẽ phải cài đặt gói npm nào để sử dụng promise = new Promise();?
Lior Erez

Mặc dù API của bạn hiện trả về một Lời hứa, nhưng bạn vẫn chưa thoát khỏi kim tự tháp của sự diệt vong hoặc làm ví dụ về cách các lời hứa hoạt động để thay thế các lệnh gọi lại.
Madara's Ghost

@ leo.249 Tôi không biết, bất kỳ thư viện Promise nào tuân thủ Promises / A + đều tốt. Hãy xem: Promiseaplus.com/@Bergi nó không liên quan. @SecondRikudo nếu API bạn đang giao tiếp không hỗ trợ Promisesthì bạn đang gặp khó khăn khi sử dụng lệnh gọi lại. Khi bạn vào lãnh thổ hứa hẹn, 'kim tự tháp' sẽ biến mất. Xem ví dụ mã thứ hai về cách hoạt động.
Halcyon

@Halcyon Xem câu trả lời của tôi. Ngay cả một API hiện có sử dụng các lệnh gọi lại cũng có thể được "quảng bá" thành một API sẵn sàng cho Promise, dẫn đến hoàn toàn mã sạch hơn nhiều.
Madara's Ghost

3

2019:

Sử dụng mô-đun gốc đó const {promisify} = require('util');để chuyển đổi mẫu gọi lại thuần túy cũ thành mẫu hứa hẹn để bạn có thể nhận benfit từ 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

Khi thiết lập một lời hứa, bạn có hai tham số, resolvereject. Trong trường hợp thành công, hãy gọi resolvevới kết quả, trong trường hợp thất bại, hãy gọi rejectkèm theo lỗi.

Sau đó, bạn có thể viết:

getUsers().then(callback)

callbacksẽ được gọi với kết quả của lời hứa được trả về getUsers, tức làresult


2

Sử dụng thư viện Q chẳng hạn:

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
Would, else {d.reject (new Error (err)); }, sửa lỗi đó?
Russell

0

Mã dưới đây chỉ hoạt động cho nút -v> 8.x

Tôi sử dụng phần mềm trung gian MySQL được quảng cáo này cho Node.js

đọc bài viết này Tạo phần mềm trung gian cho cơ sở dữ liệu MySQL với Node.js 8 và 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

Bạn phải nâng cấp nút -v> 8.x

bạn phải sử dụng hàm async để có thể sử dụng await.

thí dụ:

   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)
       }
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.