初始JavaScript Promises之二
非同步編程中的錯誤處理人性的、理想的也正如很多程式設計語言中已經實現的錯誤處理方式應該是這樣: try { var val = JSON.parse(fs.readFileSync("file.json"));}catch(SyntaxError e) {//json語法錯誤 console.error("不符合json格式");}catch(Error e) {//其它類型錯誤 console.error("無法讀取檔案")} 很遺憾,JavaScript並不支援上述方式,於是“聰明的猴子”很可能寫出下面的代碼: try { //code}catch(e) { if( e instanceof SyntaxError) { //handle }else { //handle }} 相信沒人會喜歡第二段代碼,不過傳統的JavaScript也只能幫你到這裡了。 上面的代碼是同步模式,非同步模式中的錯誤處理又是如何呢? fs.readFile('file.json', 'utf8', function(err, data){ if(err){ console.error("無法讀取檔案") }else{ try{ var json = JSON.parese(data) }catch(e){ console.error("不符合json格式"); } }}) 友情提醒:在node.js中你應該盡量避免使用同步方法。 仔細比較第一段和第三段的代碼的差異會發現,如此簡單的代碼竟然用了三次縮排!如果再加入其它非同步作業,邂逅callback hell是必然的了。 使用Promise進行錯誤處理假設fs.readFileAsync是fs.readFile的Promise版本,這意味著什麼呢,不妨回憶一下: fs.readFileAsync方法的返回結果是一個Promise對象 fs.readFileAsync方法的返回結果擁有一個then方法 fs.readFileAsync方法接受參數與fs.readFile一致,除了最後一個回呼函數 返回Promise對象意味著,執行fs.readFileAsync並不會立即執行非同步作業,而是通過調用其then方法來執行,then方法接受的回呼函數用於處理正確返回結果。所以使用fs.readFileAsync的使用方式如下: //Promise版本fs.readFileAsync('file.json', 'utf8').then(function(data){ console.log(data)}) OK,讓我們繼續錯誤處理這個話題。由於Promises/A+標準對Promise對象只規定了唯一的then方法,沒有專門針對catch或者error的方法,我們將繼續使用bluebird。 // 帶錯誤處理的Promise版本fs.readFileAsync('file.json', 'utf8').then(function(data){ console.log(data)}).catch(SyntaxError, function(e){ //code here}).catch(function(e){ //code here}) 上面的代碼沒有嵌套回調,和本文開始的第一段代碼的編寫入模式也基本一致。 神奇的Promisify註: 下面我們看如何對fs.readFileAsync方法進行promisify,依然是使用bluebird。 var Promise = require('bluebird')fs.readFileAsync = Promise.promisify(fs.readFie, fs) 怎麼樣,就是如此簡單!對於bluebird它還有一個更強大的方法,那就是promisify的進階版本 promisifyAll,比如: var Promise = require('bluebird')Promise.promisifyAll(fs) 執行完上面的代碼之後,fs對象下所有的非同步方法呼叫都會對應的產生一個Promise版本方法,比如fs.readFile對應fs.readFileAsync,fs.mkdir對應fs.mkdirAsync,以此類推。 另外要注意的就是,Promise版本的函數除了最後一個參數(回呼函數),其它參數與原函數均一一對應,調用的時候別忘了傳遞原有的參數。 對fs的promisification還不能令我滿足,我需要更神奇的魔法: // redisvar Promise = require("bluebird");Promise.promisifyAll(require("redis")); // mongoosevar Promise = require("bluebird");Promise.promisifyAll(require("mongoose")); // mongodbvar Promise = require("bluebird");Promise.promisifyAll(require("mongodb")); // mysqlvar Promise = require("bluebird");Promise.promisifyAll(require("mysql/lib/Connection").prototype);Promise.promisifyAll(require("mysql/lib/Pool").prototype); // requestvar Promise = require("bluebird");Promise.promisifyAll(require("request")); // mkdirvar Promise = require("bluebird");Promise.promisifyAll(require("mkdirp")); // winstonvar Promise = require("bluebird");Promise.promisifyAll(require("winston")); // Nodemailervar Promise = require("bluebird");Promise.promisifyAll(require("nodemailer")); // pgvar Promise = require("bluebird");Promise.promisifyAll(require("pg")); // ... 少年,這下你顫抖了嗎? 註:如果你正在使用mongoose,除了bluebird你可能還需要mongoomise,它的優點在於: 能夠接受任意的Promise Library (Q/when.js/RSVP/bluebird/es6-promise等等) 能夠對Model自訂靜態私人方法進行promisify,而bluebird.promisifyAll不支援 mongoomise + bluebird與僅使用bluebird效能相差無幾,可能更好。 我們幣須網已經在生產環境中使用mongoomise + bluebird,目前為止一切安好。 (未完待續 2014-07-15 23:40)