2015年1月22日 星期四

如何於 Koa 實作長輪詢(Long-polling)機制

Standard
io.js 的到來以及即將釋出的 Node.js v0.12,意味著我們不能再忽視 ECMAScript 6 的存在,更代表著 Koa Web Framework 開始要正式進入到市場了(如果你對 Koa 有興趣,可以參考『舊文』的簡報內容,有說明完整的開發方法,也有說明 Generator 的使用方法)。Koa 大量運用到 ECMAScript 6 新的 Generator 支援,簡化了非同步的機制,更減少了 Callback 的使用場景。

平心而論,Generator 的機制對許多 JavaScript 開發者確是一個不小的門檻,與舊有的習慣格格不入,需要一點時間學習及熟悉。如果你還不熟悉其機制的細節,不如先學習一些習慣的使用,這是一個比較快能上手的方式。所以,我們可以試著來實作 Long-polling(長輪詢)機制,理解如何運用 Generator 在 Koa 中進行非同步的實作。

若開發 Web 應用涉及了即時的需求,不免碰到 Long-polling(長輪詢)的機制, Long-polling(長輪詢)簡單來說,就是讓伺服器可以即時通知客戶端有資料要進行傳送,常被用在聊天室、即時通知訊息等應用,如 Facebook 的訊息通知機制,就是採用這樣的方式。

Long-polling(長輪詢)的運行原理,就是讓客戶端向伺服器要求新的資料,但取得資料後雙方都不中止連線,等到伺服器有資料要通知客戶端時才將連線中斷,而客戶端就知道有新資料要接收,重新建立一次連線以取得新資料,然後又再次保持連線不中斷,等待下次通知。

暸解了原理,我們就可以開始實作伺服器的部分。首先,假設我們現在有一個事件發送器,每次有資料更新,就會觸發一次事件。如下範例,是每秒鐘更新一次資料:

// 建立一個事件發送器
var dispatcher = new events.EventEmitter();
var num = 0;

setInterval(function() {
    // 將 num 加一,更新資料
    num++;

    // 通知所有客戶端資料有更新
    dispatcher.emit('update');
}, 1000);

如果是 Express,若要實作 Long-polling 來即時發送最新資料到客戶端,應該不外乎這樣寫:
var events = require('events');

app.get('/poll', function(req, res) {

    // 送資料到客戶端
    res.send(num);

    // 當有資料更新
    dispatcher.once('update', function() {

        // 中斷連線
        res.end();
    });
});

如果改採用 Koa 來實作:
router.get('/poll', function *() {

    // 送資料到客戶端
    this.body = num;

    // yield 將函數內的程式暫停於此,等待之後的函數完成並呼叫 callback
    yield function(callback) {

        // 當有資料更新
        dispatcher.once('update', function() {

            // 完成工作,繼續執行 yield 之後的程式碼
            callback();
        });
    };

   // Generator 跑完,連線中斷]
});
我們可以看到,Koa 都採用 Generator 來處理客戶端要求,這樣的概念與傳統非同步的 Callback 機制有些不一樣。所以,若要在 Koa 中使用一些非同步機制,就要設計一個特殊函數讓 yield 使用,以等待工作完成,使 Generator 暫停,等 yield 的工作完成後再繼續。

很多人在看到 Generator 時,不免和過去的非同步設計搞混,一時間會懷疑 yield 是否會使 JavaScript 的事件引擎阻塞,但這其實是多慮的,因為 Generator 內的機制與一般函數不一樣,而重點是 yield 也只能在 Generator 中使用,不需要怕誤用而導致事件引擎卡死。我們應該用另一個角度來看待 Generator 的邏輯,雖然他長得很像普通的函數。

客戶端實作

雖然客戶端的實作不是本文的重點,但也在此補上,供讀者參考,以下是採用 jQuery 的實作:
function poll() {
    $.ajax({
        url: '/poll',
        type: 'get',
        timeout: 120000,
        complete: poll,
        success: function(data) {
            console.log(data);
        }
    });
}

poll();