การแทนที่การโทรกลับด้วยคำสัญญาใน 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);

ฉันต้องการใช้สัญญาแทนการโทรกลับเพื่อส่งคืนข้อมูล จนถึงตอนนี้ฉันได้อ่านเกี่ยวกับคำสัญญาที่ซ้อนกันในหัวข้อต่อไปนี้: การเขียน Clean Code ด้วย Nested Promisesแต่ฉันไม่พบวิธีแก้ปัญหาใด ๆ ที่ง่ายพอสำหรับกรณีการใช้งานนี้ อะไรคือวิธีที่ถูกต้องในการกลับมาresultโดยใช้คำสัญญา?


1
โปรดดูการปรับโหนดหากคุณใช้ไลบรารี Q ของ kriskowal
Bertrand Marron

1
อาจซ้ำกันได้ของฉันจะแปลง API การเรียกกลับที่มีอยู่ให้เป็นสัญญาได้อย่างไร โปรดระบุคำถามของคุณให้เฉพาะเจาะจงมากขึ้นหรือฉันจะปิด
Bergi

@ leo.249: คุณอ่านเอกสาร Q แล้วหรือยัง? คุณได้ลองนำไปใช้กับรหัสของคุณแล้วหรือไม่หากใช่โปรดโพสต์ความพยายามของคุณ (แม้ว่าจะไม่ได้ผล) คุณติดอยู่ที่ไหนกันแน่? ดูเหมือนว่าคุณจะพบวิธีแก้ปัญหาที่ไม่ง่ายโปรดโพสต์
Bergi

3
@ leo.249 Q ไม่ได้รับการดูแล - คอมมิตล่าสุดคือ 3 เดือนที่แล้ว เฉพาะสาขา v2 เท่านั้นที่น่าสนใจสำหรับนักพัฒนา Q และนั่นก็ไม่ได้ใกล้จะพร้อมสำหรับการผลิต มีปัญหาที่ไม่ได้รับการแก้ไขโดยไม่มีความคิดเห็นในเครื่องมือติดตามปัญหาตั้งแต่เดือนตุลาคม ฉันขอแนะนำให้คุณพิจารณาคลังคำสัญญาที่ได้รับการดูแลเป็นอย่างดี
Benjamin Gruenbaum

คำตอบ:


103

การใช้Promiseคลาส

ฉันขอแนะนำให้ดูเอกสาร Promise ของ MDNซึ่งเป็นจุดเริ่มต้นที่ดีในการใช้Promise หรือฉันแน่ใจว่ามีบทเรียนออนไลน์มากมาย :)

บันทึก:เบราว์เซอร์สมัยใหม่รองรับข้อกำหนด ECMAScript 6 ของ Promises อยู่แล้ว (ดูเอกสาร 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 สนับสนุน ตอนนี้คุณสามารถประกาศฟังก์ชันว่าเป็นอยู่ได้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 และ v8 ที่ใช้โดย Node v0.12 ให้การใช้งานข้อมูลจำเพาะส่วนนี้ ใช่แล้วพวกเขาไม่ได้เป็นส่วนหนึ่งของ Node core แต่เป็นส่วนหนึ่งของภาษา
Robert Rossmann

1
สิ่งที่ควรทราบฉันรู้สึกว่าในการใช้ Promises คุณจะต้องติดตั้งแพ็คเกจ npm และใช้ require () ฉันพบแพ็กเกจสัญญาใน npm ซึ่งใช้สไตล์ bare bone / A ++ และได้ใช้สิ่งนั้น แต่ยังใหม่กับโหนด (ไม่ใช่ JavaScript)
macguru2000

นี่เป็นวิธีที่ฉันชอบที่สุดในการเขียนคำมั่นสัญญาและรหัส async ของสถาปนิกส่วนใหญ่เป็นเพราะรูปแบบที่สอดคล้องกันอ่านง่ายและอนุญาตให้ใช้โค้ดที่มีโครงสร้างสูง

31

ด้วยbluebirdคุณสามารถใช้Promise.promisifyAll(และPromise.promisify) เพื่อเพิ่ม Promise ready method ให้กับวัตถุใด ๆ

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

การเพิ่มเครื่องกำจัด

ครามสนับสนุนจำนวนมากของคุณสมบัติหนึ่งของพวกเขาคือ disposers จะช่วยให้คุณได้อย่างปลอดภัยทิ้งของการเชื่อมต่อหลังจากที่มันจบลงด้วยความช่วยเหลือของและ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 ขอบคุณขอบคุณ!
Lior Erez

2
โปรดทราบว่าการใช้คำสัญญาที่คุณตกลงที่จะใช้try-catchในการโทรแต่ละครั้ง ดังนั้นหากคุณทำค่อนข้างบ่อยและความซับซ้อนของโค้ดของคุณใกล้เคียงกับตัวอย่างคุณควรพิจารณาสิ่งนี้ใหม่
Andrey Popov

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

ตอนนี้ไม่ต้องใช้ lib ของบุคคลที่สามเพื่อทำสัญญา


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

ฉันใช้ mockup API สำหรับสัญญาของฉัน API ของคุณอาจแตกต่างออกไป หากคุณแสดง API ของคุณฉันสามารถปรับแต่งได้


2
ห้องสมุดสัญญาใดมีตัวPromiseสร้างและ.promise()วิธีการ?
Bergi

ขอขอบคุณ. ฉันแค่ฝึก node.js และสิ่งที่ฉันโพสต์คือทั้งหมดที่มีอยู่ตัวอย่างง่ายๆในการหาวิธีใช้สัญญา วิธีการแก้ปัญหาของคุณดูดี แต่สิ่งที่แพคเกจ NPM ฉันจะต้องติดตั้งเพื่อใช้งานpromise = new Promise();?
Lior Erez

แม้ว่า API ของคุณจะส่งคืน Promise ในขณะนี้ แต่คุณยังไม่ได้กำจัดปิรามิดแห่งการลงโทษหรือสร้างตัวอย่างของการทำงานของสัญญาเพื่อแทนที่การเรียกกลับ
Madara's Ghost

@ leo.249 ไม่ทราบห้องสมุด Promise ใด ๆ ที่สอดคล้องกับ Promises / A + น่าจะดี ดู: promisesaplus.com/@Bergiมันไม่เกี่ยวข้อง @SecondRikudo หาก API ที่คุณกำลังเชื่อมต่อไม่รองรับแสดงPromisesว่าคุณติดอยู่กับการใช้การโทรกลับ เมื่อคุณเข้าสู่ดินแดนแห่งพันธสัญญา 'ปิรามิด' จะหายไป ดูตัวอย่างรหัสที่สองเกี่ยวกับวิธีการทำงาน
Halcyon

@ Halcyon ดูคำตอบของฉัน แม้แต่ API ที่มีอยู่ซึ่งใช้การเรียกกลับก็สามารถ "สัญญา" ให้เป็น API ที่พร้อมใช้งานของ Promise ซึ่งส่งผลให้โค้ดสะอาดขึ้นมากโดยสิ้นเชิง
Madara's Ghost

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 (ข้อผิดพลาดใหม่ (ผิดพลาด)); } แก้ไขไหม
Russell

0

โค้ดด้านล่างใช้ได้กับโหนด -v> 8.x เท่านั้น

ฉันใช้มิดเดิลแวร์ MySQL Promisified สำหรับ Node.js

อ่านบทความนี้สร้าง MySQL Database Middleware ด้วย 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 เพื่อให้สามารถใช้ await ได้

ตัวอย่าง:

   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)
       }
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.