node.js的Promise庫-bluebird示例

前兩天公司一哥們寫了一段node.js代碼發給我,後面特地提了一句「寫的不太優雅」。我知道,他意思是回調嵌套回調,由於當時比較急也就沒有再糾結。然而心裏中總記得要解決這個問題。解決node.js的回調金字塔問題有較多方法,在《深刻淺出node.js》這本書中介紹了好幾種,有事件發佈/訂閱模式、Promise模式、async庫等。其中Promise模式被不少人推崇,實現的庫有不少,本着從衆的原則,閉着眼睛選個bluebird吧。html

然而bluebird的文檔並不咋滴,至關不咋滴!網上的例子基本上都是fs.readFile方法的示例,鮮有其餘例子。爲了更好的理解和使用bluebird,只能自已動手試一下咯。本文本着實用的目的,主要介紹如何將自定義方法轉換爲Promise方法,將異步方法轉換爲同步方法調用。node

1. 首先定義一些簡單的方法,這是一個很簡單例子,模擬讀取配置文件、打開數據庫、建立數據庫結構、建立一個用戶、讀取這個用戶、顯示這個用戶屬性的整個過程。此處就不寫node.js的回調嵌套了,以避免使用手機打開本文時特別慘不忍睹的。sql

 1 //數據庫對象
 2 var db;
 3 
 4 //使用配置文件獲取鏈接字符串
 5 var getConn = function(cfg){
 6 }
 7 
 8 //建立或打開sqlite3數據庫
 9 var openDb =  function(dbConn){
10 }
11 
12 //建立數據庫結構
13 var createSchema = function(){
14 }
15 
16 //建立用戶
17 var createUser = function(){
18 }
19 
20 //獲取用戶
21 var getUser = function(id){
22 }
23 
24 //顯示用戶屬性
25 var showUser = function(user){
26 }

2. 首先來看使用bluebird怎麼將異步方法變成同步方法執行數據庫

"use strict";

var fs = require("fs");
var sqlite3 = require("sqlite3");
var Promise = require("bluebird");

const conn = "conn.txt";
var db;

var getConn = function(cfg){
    return new Promise(function(resolve, reject){
        fs.readFile(cfg, "utf-8", function(err, data){
            if(err){
                reject(err);
            } else {
                console.log("db: ".concat(data));
                resolve(data.trim());
            }
        });
    });
}

var openDb = function(dbConn){
    return new Promise(function(resolve, reject){
        db = new sqlite3.Database(dbConn, function(err){
            if(err){
                reject(err);
            } else{
                console.log("open database");
                resolve();
            }
        });
    });
}

var createSchema = function(){
    return new Promise(function(resolve, reject){
        db.serialize(function(){
            var createExpsTable = "CREATE TABLE IF NOT EXISTS expressions ('name' NVARCHAR(20), 'expression' TEXT, 'index' INT, 'likes' INT)";
            var createUserTable = "CREATE TABLE IF NOT EXISTS users ('name' NVARCHAR(20), 'password' VARCHAR(20))";
            db.exec(createExpsTable, function(err){
                if(err){
                    reject(err);
                } else {
                    console.log("create table expressions");
                }
            });

            db.exec(createUserTable, function(err){
                if(err){
                    reject(err);
                } else {
                    console.log("create table users");
                    resolve();
                }
            });
        });
    });
}

var createUser = function(){
    return new Promise(function(resolve, reject){
        db.run("INSERT INTO users (name, password) VALUES ($name, $password)", {$name: "think8848", $password: "111111"}, function(err){
            if(err){
                reject(err);
            } else{
                console.log("createUser");
                resolve(this.lastID);
            }
        });
    });
}

var getUser = function(id){
    return new Promise(function(resolve, reject){
        db.get("SELECT rowid, name, password FROM users WHERE rowId = $id", {$id: id}, function(err, row){
            if(err){
                reject(err);
            } else {
                console.log("getUser");
                resolve(row);
            }
        });
    });
}

var showUser = function(user){
    console.log("id: ".concat(user.rowid).concat(", name: ").concat(user.name).concat(", password: ").concat(user.password));
}

getConn(conn)
.then(openDb)
.then(createSchema)
.then(createUser)
.then(getUser)
.then(showUser)
.catch(function(err){
    console.log(err.message);
});
View Code

 查看執行結果,能夠看到徹底沒有問題,全部方法都按照設想流程在執行。express

可是會不會有一種可能,數據過小,電腦執行的很快,因此剛好在下一個方法執行以前上一個方法的異步已經執行完成了(這以前也遇到過這種問題),咱們經過 setTimeout 來驗證一下:把 createUser 方法延遲1000毫秒再執行,看看 getUser 是否還能獲取到數據c#

var createUser = function(){
    return new Promise(function(resolve, reject){
        setTimeout(function(){
            console.log("delay 1000ms");
            db.run("INSERT INTO users (name, password) VALUES ($name, $password)", {$name: "think8848", $password: "111111"}, function(err){
                if(err){
                    reject(err);
                } else{
                    console.log("createUser");
                    resolve(this.lastID);
                }
            });
        }, 1000);
    });
}

查看執行結果,徹底沒有問題, getUser 方法並無偷偷提早執行異步

3. 在剛開始接觸bluebird的時候,我有不少疑問。async

其中有一個就是:是否僅需將第一個要執行的異步方法實現爲Promise模式,其餘的方法只需簡單的放到 .then() 方法便可?咱們來進行實驗一下,這裏爲了代碼結構簡單點,我僅演示模擬模擬讀取配置文件、打開數據庫、建立數據庫結構、建立一個用戶流程,也很能說明問題了。 ide

"use strict";

var fs = require("fs");
var sqlite3 = require("sqlite3");
var Promise = require("bluebird");

const conn = "conn.txt";
var db;

var getConn = function(cfg){
    return new Promise(function(resolve, reject){
        fs.readFile(cfg, "utf-8", function(err, data){
            if(err){
                reject(err);
            } else {
                console.log("db: ".concat(data));
                resolve(data.trim());
            }
        });
    });
}

var openDb = function(dbConn){
    db = new sqlite3.Database(dbConn, function (err) {
        if (err) {
            throw err;
        } else {
            console.log("open database");
        }
    });
}

var createSchema = function(){
    db.serialize(function () {
        var createExpsTable = "CREATE TABLE IF NOT EXISTS expressions ('name' NVARCHAR(20), 'expression' TEXT, 'index' INT, 'likes' INT)";
        var createUserTable = "CREATE TABLE IF NOT EXISTS users ('name' NVARCHAR(20), 'password' VARCHAR(20))";
        db.exec(createExpsTable, function (err) {
            if (err) {
                throw err;
            } else {
                console.log("create table expressions");
            }
        });

        db.exec(createUserTable, function (err) {
            if (err) {
                throw err;
            } else {
                console.log("create table users");
            }
        });
    });
}

var createUser = function(){
    db.run("INSERT INTO users (name, password) VALUES ($name, $password)", { $name: "think8848", $password: "111111" }, function (err) {
        if (err) {
            throw err;
        } else {
            console.log("createUser");
        }
    });
}

getConn(conn)
.then(openDb)
.then(createSchema)
.then(createUser)
.catch(function(err){
    console.log(err.message);
});
View Code

查看執行結果,貌似也沒有問題,所有都按照想像中的順序執行了,是真的嗎?函數

仍是再經過 setTimeout 方法驗證下,若是將建立數據庫結構的時間推遲,是否還能正確建立用戶呢?

var createSchema = function(){
    setTimeout(function(){
        db.serialize(function () {
            var createExpsTable = "CREATE TABLE IF NOT EXISTS expressions ('name' NVARCHAR(20), 'expression' TEXT, 'index' INT, 'likes' INT)";
            var createUserTable = "CREATE TABLE IF NOT EXISTS users ('name' NVARCHAR(20), 'password' VARCHAR(20))";
            db.exec(createExpsTable, function (err) {
                if (err) {
                    throw err;
                } else {
                    console.log("create table expressions");
                }
            });

            db.exec(createUserTable, function (err) {
                if (err) {
                    throw err;
                } else {
                    console.log("create table users");
                }
            });
        });
    }, 1000);
}

查看執行結果:出錯了,提示沒有找到users表,這說明建立用戶方法的執行時間要早於建立數據庫結構的執行時間。這代表若是要確保每一個方法都順序執行,那就必須每一個方法都是Promise模式

爲了更好的看清楚Promise的執行順序,下面再次用一個簡單的例子和運行結果來展現這個問題

"use strict";

var Promise = require("bluebird");

var first = function(){
    console.log("first");
};

var second = function(){
    console.log("second");
}

var third = function(){
    console.log("third");
}

Promise.resolve().then(first).then(second).then(third);

查看執行結果

修改 second 方法爲異步方法

var second = function(){
    setTimeout(function () {
        console.log("second");
    }, 1000);
}

查看執行結果,發現執行順序已經錯了

修改 second 方法爲 Promise 方法

var second = function(){
    return new Promise(function(resolve, reject){
        setTimeout(function(){
            console.log("second");
            resolve();
        },1000);
    });
}

查看執行結果,發現順序又和預期同樣了

4.  每一個Promise方法都使用這種寫法好像有點麻煩,是否有更好的辦法呢?在不少bluebird的例子中都給了答案,使用promisify方法,下面咱們來看改造後的例子。這裏值的一提是的,經實驗發現,若是要 promisify 一個方法(這個方法被bluebird官方稱之爲 nodeFunction ),那麼這個方法就必須知足如下簽名: function(any arguments..., function callback) nodeFunction ,即:有兩個參數,第一個參數是上一個Promise執行後的返回值,第二個參數是回調方法,及時上一個方法沒有返回值,那麼第一個參數也是不該該省去的。儘量不要給這個 nodeFunction 方法提供多個參數,若是上一個方法有多個返回值,那麼最好將多個返回值封裝爲一個對象返回。

"use strict";

var fs = require("fs");
var sqlite3 = require("sqlite3");
var Promise = require("bluebird");

const conn = "conn.txt";
var db;

var openDb = function(dbConn, callback){
    db = new sqlite3.Database(dbConn.trim(), function (err) {
        if(!err){ 
            console.log("open database");
        }       
      return  callback(err);
    });
}

var createSchema = function(args, callback){
    db.serialize(function () {
        var createExpsTable = "CREATE TABLE IF NOT EXISTS expressions ('name' NVARCHAR(20), 'expression' TEXT, 'index' INT, 'likes' INT)";
        var createUserTable = "CREATE TABLE IF NOT EXISTS users ('name' NVARCHAR(20), 'password' VARCHAR(20))";
        db.exec(createExpsTable, function (err) {
            if(err){
                callback(err);
            }
            console.log("create table expressions");  
        });

        db.exec(createUserTable, function (err) {
            if (!err) {
                console.log("create table users");
            } 
            callback(err);
        });
    });
}

var createUser = function(args, callback){
    db.run("INSERT INTO users (name, password) VALUES ($name, $password)", { $name: "think8848", $password: "111111" }, function (err) {
        if (!err) {
            console.log("createUser");
        }
        //此處向下一個Promise方法提供參數值
        callback(err, this.lastID);
    });
}

var getUser = function(id, callback){
    db.get("SELECT rowid, name, password FROM users WHERE rowId = $id", { $id: id }, function (err, row) {
        if (!err) {
            console.log("getUser");
        }
        callback(err, row);
    });
}

var showUser = function(user){
    console.log("id: ".concat(user.rowid).concat(", name: ").concat(user.name).concat(", password: ").concat(user.password));
}

var getConnAsync = Promise.promisify(fs.readFile);
var openDbAsync = Promise.promisify(openDb);
var createSchemaAsync = Promise.promisify(createSchema);
var createUserAsync = Promise.promisify(createUser);
var getUserAsync = Promise.promisify(getUser);

getConnAsync(conn, 'utf-8')
.then(openDbAsync)
.then(createSchemaAsync)
.then(createUserAsync)
.then(getUserAsync)
.then(showUser)
.catch(function(err){
    console.log(err);
});
View Code

查看執行結果:徹底沒有問題,妥妥的按照既定的順序來了。

爲了保險,咱們再使用 setTimeout 進行驗證

var createUser = function(args, callback){
    setTimeout(function () {
        console.log("delay 1000ms");
        db.run("INSERT INTO users (name, password) VALUES ($name, $password)", { $name: "think8848", $password: "111111" }, function (err) {
            if (!err) {
                console.log("createUser");
            }
            //此處向下一個Promise方法提供參數值
            callback(err, this.lastID);
        });
    } ,1000);
}

驗證結果:能夠看出依舊是按照順序執行的

 咱們再看一個例子:

"use strict";

var Promise = require("bluebird");

function first(cb){
    var str = "first";
    console.log("begin");
    cb(null, str);
}

function second(data,cb){
    var str = "second";
    console.log(data);
    cb(null, str);
}

var firstAsync = Promise.promisify(first);
var secondAsync = Promise.promisify(second);

firstAsync().then(secondAsync).then(console.log);

其執行結果以下:

仔細觀察咱們會發現這個例子中對兩個方法使用了promisify方法,按照上面的說明,這兩個方法的籤應符合 nodeFunction 約定纔是,然而第一個方法僅包含一個回調函數參數,並無包含值參數,咱們嘗試着加一個:

function first(args, cb){
    var str = "first";
    console.log("begin");
    cb(null, str);
}

執行結果以下:驚訝的發現第一個參數是回調函數,而第二個參數爲undefined(此處使用的是vscode的調試功能,畢竟是c#er,感受vscode仍是很是好用)

想都不用想,爲 first 方法提供一個 null 參數確定能解決問題,然而感受實在仍是太奇怪了。

能夠嘗試用稍優雅點的方法來處理,用一個 Promise.resolve() 空方法前導一下

相關文章
相關標籤/搜索