Tôi sẽ viết một bài "Tại sao cần chuyển sang lập trình Promise" thay vì lập trình callback truyền thống. Thực ra bài này khó viết vì phải liệt kê, nêu bật các vấn đều callback qua các ví dụ thực. Để hiểu vể Promise thì các bạn tham khảo trước bài viết PROMISE - NÀY THÌ COMEBACK VERSION 2 - ASYNC THÌ SAO NÀO? của đồng chí Ricky, tuổi trẻ tài cao. Mình đọc bài của đồng chí này thấy rất sướng!. 

Bài này và một số bài tiếp theo sẽ chuyên hướng dẫn lập trình Promise sử dụng thư viện BlueBird của tác giả Petka Antonov. Tại sao dùng BlueBird?

  • BlueBird tuân thủ chuẩn Promise/A+. Trong tương lai JavaScript chạy front end hay back end sẽ đều hỗ trợ sẵn. Code hôm nay viết cho BlueBird không phải sửa khi chuyển qua Promise/A+ mặc định của JavaScript.
  • Tốc độ BlueBird cao nhất trong các module tuân thủ chuẩn Promise/A+.
  • Chức năng đầy đủ, đến giờ chưa thấy thiếu gì.

Để sử dụng BlueBird, gõ lệnh muôn thủa này vào terminal npm install bluebird --save

Đọc một file JSON trong cùng thư mục rồi parse và in nó ra

Viết theo kiểu call back như dưới đây

var fs = require('fs');
fs.readFile("bad.json", function(err, val) {
    if(err) {
        console.error("unable to read file");
    } else {
        try {
            var parse = JSON.parse(val);
            console.log(parse);
        } catch(e) {
            console.error("invalid json in file");
        }
    }
});

Do module fs chưa hỗ trợ promise, nên ta sử dụng chức năng promisify để hàm call back cổ điển tạo mới, rồi trả về Promise gồm 2 hàm fullfill (thành công) và reject (thất bại)

var fs = require('fs');
var promise = require('bluebird');
promise.promisifyAll(fs); //Chuyển tất cả các hàm trong fs sang dạng Promise

fs.readFileAsync("good.json").then(JSON.parse).then(function(json) {
    console.log(json);
}).catch(SyntaxError, function(e) {
    console.error("invalid json in file", e.message);
}).catch(function(e){
    console.error("unable to read file", e.message);
});

Chú ý lệnh promise.promisifyAll(fs); sẽ chuyển tất cả hàm trong module fs sang dạng Promise. Chỉ nên dùng lệnh này khi bạn sử dụng rất nhiều hàm của fs, vì lệnh này rất chậm, trên máy của tôi, nó tốn 31 mili giây. Bạn nên thay thế bằng lệnh này, nó chỉ chuyển duy nhất hàm fs.readFile, chạy mất đúng 2 mili giây!

var fs = require('fs');
var promise = require('bluebird');
console.time('xx');
var readFileAsync = promise.promisify(fs.readFile);  //chỉ convert duy nhất hàm fs.readFile
console.timeEnd('xx');  //Mất khoảng 2 milisecond
readFileAsync("bad.json").then(JSON.parse).then(function(json) {
    console.log(json);
}).catch(SyntaxError,function(e) { //Bắt lỗi parse JSON trước
    console.error("invalid json in file", e.message);
}).catch(function(e){ //Bắt lỗi đọc file
    console.error("unable to read file", e.message);
});

Code không viết ngắn được nhiều lắm. Nhưng nhìn dễ hiểu hơn: đọc file xong then thì parse JSON xong then in ra console. Còn lại thì sẽ bắt lỗi ở cuối. Lỗi chi tiết bắt trước, lỗi chung chung để sau cùng. Cái hay là tất cả các tác vụ được thực hiện tuần tự nhưng không ngăn tiến trình xử lý event queue chạy (non-blocking).

Thêm ví dụ nữa: hãy tải về một file ảnh ở trang web https://unsplash.com/ sau đó ghi vào file dog.jpg ở cùng thư mục

Đoạn code dưới đây sử dụng callback cổ điển. Đã được đóng gói khá đẹp sử dụng .on('event',....) để xử lý kết quả trả về hoặc lỗi.

var fs = require('fs');
var request = require('request'); //Sử dụng request để tạo httpClient

console.time('download');
request.get('https://unsplash.imgix.net/photo-1425235024244-b0e56d3cf907?fit=crop&fm=jpg&h=700&q=75&w=1050')
    .on('error', function(err) {
        console.log('Download error', err);
    })
    .pipe(fs.createWriteStream('dog.jpg')
        .on('finish', function(){
            console.timeEnd('download');
            console.log('Done write to file');
        }).on('error', function(err){
            console.log('Error write to file: ', err);
        })
    );

Chuyển sang Promise, BlueBird sẽ trông như dưới. Hàm getPhoto sẽ tạo ra Promise. Trong Promise này nếu thành công thì gọi hàm fullfill và ngược lại gọi hàm reject để xử lý lỗi. Tham số truyền vào fullfil là giá trị cần trả về để xử lý trong lệnh tiếp theo. Tham số truyền vào reject là đối tượng lỗi, Error.

var fs = require('fs');
var request = require('request');
var promise = require('bluebird');
var photoLink = {link: 'https://unsplash.imgix.net/photo-1425235024244-b0e56d3cf907?fit=crop&fm=jpg&h=700&q=75&w=1050',
    name: 'dog.jpg'};
//Tự viết hàm wrapper trả về Promise nhận vào 2 tham số fullfill, reject
function getPhoto(photoLink){
    return new Promise(function(fulfill, reject) {
        request.get(photoLink.link)
            .on('error', function (err) {
                err.photo = photoLink.link;
                reject(err);
            })
            .pipe(fs.createWriteStream(photoLink.name)
                .on('finish', function () {
                    fulfill(photoLink.name);
                }).on('error', function (err) {
                    reject(err);
                })
        );
    });
}
//Giờ có thể sử dụng hàm getPhoto theo kiểm thenable!
getPhoto(photoLink)
    .then(function(result){
        console.log('Done write to file', result);
    }).catch(function(err){
        console.log('Error: ', err.message);
    });

Kết luận được gì sau 2 ví dụ trên?

  1. Chuyển sang viết Promise, code không ngắn đi so với callback nhiều lắm. Chỉ ngắn hơn khi hàm Promise được tái sử dụng nhiều lần.
  2. Code kiểu Promise sẽ dễ đọc hơn: Làm A, sau đó thì làm B, sau đó thì làm C, nếu có lỗi X thì xử lý thế này, có lỗi Y xử lý thế kia.
  3. Có thể biến một module không hỗ trợ promise bằng lệnh promisifyAll('tên module') cái này tốn thời gian để wrap tất cả các hàm. Tối ưu hơn thì dùng promisify('module.function')
  4. Có thể viết hàm gồm nhiều logic phức tạp : tải file từ site trên mạng --> ghi file thành một hàm chuẩn Promise. Sau này sẽ có nhiều trò hay ho. Xem bài sau sẽ rõ.

Chờ bài tiếp theo nhé. Toàn bộ code đã chạy thử trên iojs 1.5 trên máy Mac. Anh em thử chạy thử xem sao nhé.