Koa 2 起手式!



在 Node.js 的世界裡,說到今天最潮的 Web Framework,肯定就是 Koa!其用了最新的 JavaScript 語法和特性,來改善 Web Framework 的設計。只不過,Koa 雖然相對於其他舊的 Web Framework 來說有相當多的進步,但很多人卻相當討厭 Koa 的 Generator 設計,尤其是那些「*」符號,那不知所謂的 yield 也讓很多人不舒服。所以至今仍然有許多人在使用 express 來當作自己的 Web Framework,寧可繼續使用那老派的 callback 設計,而不肯嘗試 Koa。

隨著 ECMAScript 標準的進步,Koa 才剛被開發出來沒多久,原本的開發團隊就立即著手打造 Koa 2 ,開始更進一步採用更新的 JavaScript 特性,以 async/await 語法重新打造了全新且更簡潔的框架。可惜的是,由於 async/await 語法一直遲遲沒有被 JavaScript 引擎原生支援,因此總需要靠 babel 編譯打包程式碼後,才能正常跑在 Node.js 之上。這讓 Koa 2 一直無限期處於非穩定版,讓原開發者從開發的一開始,就打算等到 V8 和 Node.js 開始原生支援 async/await 後,才會被以穩定版(stable)的姿態釋出。

所以,即使 Koa 2 到了今天已經相當穩定,也開始有不少人使用在自己的線上服務,卻一直無限期處於非穩定版的狀態。

另外,由於 Koa 2 大量使用 Async/Await,如果你還對 Async/Await 的使用還不熟悉,建議在閱讀本文之前,先閱讀舊文「JavaScript 好用的 async 異步函數!」來學習如何使用。

學習 Koa 的好時機來囉


總算,日前 Node.js v7.6.0 釋出後已經正式宣布原生支援了 async/await 語法,而且不需要額外的參數選項。伴隨著這個消息,Koa 2.0 也隨即正式釋出了!

Node.js 內建支援 ES7 的 async/await 真的是非常棒的消息!過去我們使用 async/await,都還需要 babel 的協助才能正常跑在舊版的 Node.js,不但開發上相當麻煩,非原生的各種 ES7 特性也浪費不少額外的記憶體和效能,這樣的問題在斤斤計較效能的 Server 環境下,更是讓人頭痛。

如今 Node.js 的原生支援,讓我們已經不需要再擔心種種問題,讓我們可以得到簡潔的程式碼和兼顧效能,現在就是準備轉換到 Koa 2 的最好時機!:-)

安裝 Koa 2


現在,我們終於可以直接使用 NPM 命令安裝 Koa 2:

npm install koa

開發第一個應用程式


如果你有開發過 Koa 或 Express 的網站應用程式,Koa 2 的寫法其實相當雷同,差別是 Express 使用的是普通函數當 callback、Koa 是使用 Generator Function,而 Koa 2 是使用 Async Function。

一個簡單的範例如下:

const Koa = require('koa');

const app = new Koa();

app.use(async function(ctx) {
    ctx.body = 'Hello World';
});

app.listen(3001);

當 ctx.body 被設定一個內容後,連線就會回傳該內容回瀏覽器。在這範例中,無論發什麼要求給 Server ,都會得到「Hello World」的回傳。

註:如果你使用過 Koa,會發現 Koa 2 已經不再使用 this 關鍵字,而是改成一個 context 物件代入到函數之中。

使用異步函數打造的 Middleware


koa.use() 將用來載入 Middleware,所有連線工作都會經過 Middleware 處理。這也是為什麼,前一個例子裡,我們使用 koa.use() 設定了一個處理函數後,所有連線透會通過該函數進行處理並回傳同樣的值。

要注意的是,該函數是一個「異步函數(Async Function)」,要用到 async 關鍵字來宣告:

app.use(async function() {
    // ...
});

註:如果你有過 express 開發經驗,對於 koa.use() 會相當熟悉,Koa 同樣支援了 Middleware 的架構,你可以將過去的程式輕易移植到這新的框架上。

自訂 Router 和路徑管理


之前的範例直接使用 koa.use(),會將所有的連線都導入同一個處理函數,輸出同一個結果。若我們想要自訂不同的路徑,讓不同路徑用不同的處理函數,將需要額外安裝「koa-router」模組:

npm install koa-router

然後可以用 koa-router 來管理對外的連線路徑:

const Koa = require('koa');
const Router = require('koa-router');

const app = new Koa();
const router = Router();

// 設定根路徑的處理函數
router.get('/', async function(ctx) {
    ctx.body = 'Hello World';
});

app.use(router.routes());

app.listen(3001);

接收 QueryString 資料


QueryString 可說是歷史悠久且非常常見的傳值方法,藉由一個網址後面加上一個「?」字元後,就可以使用鍵值(Key/Value)來進行資料傳遞,並用「&」區隔多組資料。一個簡單的實際應用如下:

http://my_server/send?name=fred&msg=Hello

取得資料的方法如下:

ctx.query.name
ctx.query.msg

接收 body 資料


當我們使用「POST」或「PUT」方法,我們就可以利用 body 傳送一些資料到伺服器,像是網頁表單時常使用這樣的傳值方法。若想要取得 body 的資料,必須先安裝「koa-bodyparser」模組。

截至本文釋出為止,該模組還沒有隨著 Koa 2 推出正式支援的版本,所以預設下載回來的版本還是支援舊的 Koa,所以必須指定版本號「next」:

npm install koa-bodyparser@next

當然,你也可以用「koa-convert」模組將舊的 Koa Middleare 直接轉換給 Koa 2 使用:

npm install koa-convert

使用 koa.use() 載入 koa-bodyparser,koa 就會自動在處理連線時使用它解析 body:

var bodyParser = require('koa-bodyparser');

// 若想使用 koa-convert 進行轉換,要先載入模組:
// const convert = require('koa-convert');
// 再以 convert(bodyParser()) 包裝
app.use(bodyParser());

然後可以在路徑處理函數中,正常取得 body 內的資訊:

ctx.request.body.name
ctx.request.body.msg

錯誤處理


在 Koa 2 中,可以透過 ctx.throw() 進行錯誤處理,並回傳狀態值和內容給客戶端,他會中斷目前的處理函數,實際使用情境如下:


router.get('/api/v1/user', async function(ctx) {

    // 檢查 Token,若有問題回傳 400 HTTP StatusCode
    if (ctx.query.token == '123')
        ctx.throw(400);

    // 若已經拋出 400 的狀態,接下來的程式不會被執行
    ctx.body = 'Hello World';
});

加入多個 Middleware


所有的連線要求可以透過一系列、不只一個 Middleware 來進行處理,我們可以利用多次 koa.use() 來加入多個 Middleware,多個 Middleware 可以用來做到很多功能,例如記錄和顯示每個連線的狀態。

加入多個 Middleware 的範例如下:

const Koa = require('koa');

const app = new Koa();

app.use(async function(ctx, next) {
    // 略過這個 Middleware,讓下一個 Middleware 來接著處理
    await next();
});

app.use(async function(ctx) {
    ctx.body = 'Hello World';
});

app.listen(3001);

加入 Logger 來記錄連線要求


koa-logger 是一個能顯示連線要求狀態的第三方 Middleware,可以先透過 NPM 安裝它:

npm install koa-logger

然後可以直接以 app.use() 引用:

const Koa = require('koa');
const logger = require('koa-logger');

const app = new Koa();

// 加入 logger 在其他的 Middleware 之前
app.use(logger());

app.use(async function(ctx) {
    ctx.body = 'Hello World';
});

app.listen(3001);

然後,你的應用程式就會輸出漂亮的連線要求訊息:

  <-- GET /api/v1/hello
  --> GET /api/v1/hello 200 8,257ms 2b

在 Koa 2 裡使用 Mongoose


異步函數使用 await 關鍵字對 Promise、Thunk 進行等待,使開發者不再需要用到大量的 callback function,讓程式碼比較不會「橫著長大」。所以,只要 Mongoose 可以在做任何工作時,回傳一個 Promise 物件,我們就可以在 Koa 2 中使用 await 等它完成。

還好,Mongoose 有支援這個功能,但我們得使用 .exec() 這個方法來取得 Promise:

router.get('/api/v1/user', async function(ctx) {

    // 利用 exec() 取得 Promise,然後以 await 等待完成
    ctx.body = await Users.find({}).exec();
});

函數宣告的習慣改變


在本文的範例中,仍然還是使用「function()」這樣的函數宣告方式,但很多開發者為了減少程式碼,大量改用 Arrow Function(箭頭函數)來宣告函數,所以你會大量看到這樣的情況:

router.get('/api/v1/user', async (ctx) => {
    // ...
});

因為多數情況下,改用 Arrow Function 來宣告是沒有問題的,所以很多懶惰的開發者都會這樣使用。但建議你,如果有空時,還是要了解這種函數與普通函數宣告的差別。

後記


這篇文章其實卡很久了,一直遲疑著要不要在正式穩定版之前公開,剛好趁著這幾天 Node.js v7.6.0 和 Koa 2 正式版釋出,所有的顧慮就沒有啦。:-D

留言

這個網誌中的熱門文章

有趣的邏輯問題:是誰在說謊

Web 技術中的 Session 是什麼?

淺談 USB 通訊架構之定義(二)

淺談 USB 通訊架構之定義(一)

Reverse SSH Tunnel 反向打洞實錄