2015年1月23日 星期五

Koa 的非同步流程設計

Standard
Koa 採用了 Generator 來控制非同步流程,所以在採用 Koa 的程式碼上,我們會看到很多 yield 關鍵字的出現。某程度來說,Generator 讓程式碼變得乾淨許多,也平坦許多,比較少出現太複雜的 Callback 高山。這對 JavaScript 來說是件不錯的事,只是,對很多開發者來說,需要點時間熟悉其概念。

總而言之,所有 yield 的關鍵字,一定只會出現在 Generator 當中,而一個 Generator 長得會是一個以『*』符號開頭的函數模樣:
function *() {
    // 工作流程
}

所以若是你仔細觀察,就會發現 Koa 的路由處理,採用的不是 callback 來處理客戶端要求,而是 Generator:
router.get('/test', function *() {
    // 處理客戶端要求的工作
});

其實不久前,舊文『如何於 KOA 實作長輪詢(LONG-POLLING)機制』已提及怎麼來掌控 Koa 的流程,雖然只是簡單提及,但已經大略展示其使用方法。簡單來說,你可以當作這個 Generator 結束後,與客戶端的連線就會結束,所以我們如果要處理非同步的工作,也必須避免和阻止這個 Generator 執行完。

使用 yield 控制 Generator 的流程

為了阻止 Generator 一下就跑完,我們會使用 yield 方法來暫停 Generator 的執行,並等待一個需要花時間的非同步工作完成。如果你對舊文的內容有印象就會知道,想要使用 yield 在 Koa 中去呼叫一個非同步機制,就需要設計一個特別的函數,其函數有一個 callback,當這個 callback 被呼叫時,代表工作完成。

如下面範例就是想要執行一個非同步函數 setTimeout(),並等待其執行完成。在這範例中,我們加上了 console.log() 印出 START 和 END 字串,便於觀察其行為:
router.get('/test', function *() {

    console.log('START');

    // 暫停 Generator 並等待工作完成
    yield function(done) {
        setTimeout(function() {
            console.log('TIMEOUT');

            // 完成,呼叫 callback 告訴 Generator 可以繼續 yield 以後的工作
            done();
        }, 1000);
    };

    console.log('END');
});

依照以上的例子,如果你用瀏覽器發送要求後,可以在 Server 端馬上看見 START 被印出來,然後經過一秒後會先出現 TIMEOUT 再出現 END。

很多人可能會覺得頭痛,因為照過去 JavaScript 的非同步概念來看,順序應該是要 START、END 然後等待一秒鐘後才是 TIMEOUT。但在 Generator 中,使用了 yield 以後並不是如此,所以你可以把它看作是一個在 JavaScript 中的特殊領域,以同步化(Synchronous)會阻塞(Block)的概念來進行邏輯設計的存在。

錯誤處理

JavaScript 一直以來,最讓人詬病的就是非同步機制的錯誤處理很麻煩,也很囉唆。在使用 Generator 之後,我們可以直接用 try-catch 的方法進行錯誤處理。如下所示:

try {
    yield function(done) {
        setTimeout(function() {
            // 拋出錯誤
            done(new Error('WRONG'));
        }, 1000);
    }
} catch(err) {
    // 處理錯誤
    console.log(err);
}

Koa 的設計上,只要將錯誤代入到 callback 的第一個參數,使第一個參數不是 null,就會拋出錯誤,讓 yield 外的 try-catch 去攔截,如此就可以很容易來處理非同步機制的錯誤。

從 yield 得到回傳結果

如果已經瞭解了 yield 的使用方式和邏輯,我們也可以等待非同步工作將一些結果回傳,就像下面的使用方式:
var result = yield function(done) { ... }

在 Koa 中,確實作法只需要代入想要回傳的資料到 callback 的第二個參數即可:

var result = yield function(done) {
    setTimeout(function() {
        // 拋出錯誤
        done(null, 'hello');
    }, 1000);
}
console.log(result);

包裝成更好用的函數

我們固然可以直接使用匿名函數,讓 Generator 去執行,但這可能不是一個好的做法,多數情況,我們還是會將功能包裝成 API 函數的形式,增加重複利用的機會,如下:
yield delay(3000);

如果你很熟悉 JavaScript 的開發,應該已經知道怎麼做,邏輯如前面所提及的範例一樣,只是額外進行一層包裝罷了:
function delay(interval) {
    return function(done) {
        // 照 interval 變數所給的數值,決定暫停時間長度
        setTimeout(done, interval);
    };
}

後記

Generator 是一個會讓傳統 JavaScript 開發者抓狂的東西,不過 Koa 對其已經做了初步的包裝,只要熟悉 callback 的使用,都不會有太大問題。但這也代表 Koa 中所用到的 Generator 機制,和原始的 Generator 使用方法會有些差別,最大差別就是沒有 callback 的設計,得自己實作。

所以,如果你是一個 Generator 的初學者,建議先不要直接學習原始 Generator 的使用方法,可以藉由 Koa 先熟悉並瞭解 Generator 的邏輯。等到都熟悉了 Generator 的概念後,再開始學習怎麼自行實作如同 Koa 的 callback。