What is Koa?

相信很多人對NodeJS的第一次接觸就是Http Server,Koa是一個十分新潮的Http框架,他支援ES6甚至ES7的新特性,更容易debug,表達力更加強大,程式碼更加精簡。

相信很多人使用過Express。Express也是許多NPM套件的依賴,社群也很活躍,幾乎遇到的問題都已經有人遇過了,因此可以很容易找到解答。Koa是由Express的開發者另起爐灶做出來的框架,因為兩者在根本的概念上差異過大,所以才有此分歧。我不認為Koa會取代掉Express,因為Koa使用的門檻比較高一些,使用的新語法可能會對剛加入這個領域的人覺得困惑,但是如果之前就接觸過NodeJS的使用者,Koa絕對是值得嘗試的,因為他擁有許多迷人的優點。

Koa vs Express vs Connect

@來自koa source tree的比較表

Feature Koa Express Connect
Middleware Kernel
Routing
Templating
Sending Files
JSONP

由上表可知Koa是個相對精簡,並且高度模組化的框架,它預設只內涵一個中間件核心,我們需要什麼功能就將那個功能的中間件給安插進來,就像是玩積木一般的組裝一個最精簡的http server。這對於現代的Http server有著顯著的優勢,因為純前端的框架崛起,後端不再需要那麼多的功能,Koa在這方面能夠將我們所需要的功能,但是盡可能的最小化。

接下來的教學中會盡量涵蓋多點範例程式碼,並且下更多註解來幫助理解,因此如果沒有使用過其他前框架依舊可以從零開始學習。

注意: 因為Koa需要ECMAScript新特性的關係,我們NodeJS v7.6.0以上才得以執行。
如果NodeJS版本過舊,可以使用babel來進行轉譯。但是我個人更喜歡的方法是使用Typescript。
本篇文章也會以Typescript的方式來撰寫範例程式。

環境架設

使用yarn或者npm安裝koa套件yarn add koa @types/koanpm install koa @types/koa --save

Hello World

我們來看一下Koa的Hello world

    
// Hello.ts
import Koa = require('koa');            // 引入koa模組

const app = new Koa();                  // 建立koa實例

app.use(async (ctx) => {                // 中間件
    ctx.body = '<h1>Hello Koa</h1>';    // 回傳資料給與前端
});

app.listen(3000);                       // 監聽Port 3000

將檔案存成app.ts,然後使用ts-node app.ts指令即可瀏覽http://localhost:3000/看到我們的預期結果。

Async function

使用async function是Koa的一大亮點,async function也是ES7新標準中對於ECMAScript的一項十分重大的改進。

阮一峰老師文章中有一句我非常認同的話: 异步编程的最高境界,就是根本不用关心它是不是异步

Async function是改善了ECMAScript多年以來異步編程的缺點,內化改良後衍生出來的精品,糅合了Generators以及Promise的各種優勢,讓我們能夠忘記自己正在寫的是異步程序,而達到與異步編程相同的效率。

在寫網站的同時我們會不斷地做一些需要異步處理的工作,例如檔案存取,資料庫的存取,這些操作如果使用Async function來控制流程,那是一件十分舒服的事,程式碼也會非常漂亮。

這邊不會深加討論async function的用法以及理論,我會將參考資料放在文末,可以參考這些資料來了解這個強悍的新功能。

我這邊僅用一個簡單的範例來比較一下使用async function與不使用async function所帶來程式碼的不同,進行一下比較。

我們寫一個類似于上個範例Hello Koa的例子,不過我們這次回傳的資料使用的是事先寫好的html檔,因此我們必須先讀取這個檔案,再將這個檔案的內容回傳回去給前端。

使用Async function

    
// async.ts
import Koa = require('koa');
import fs = require('fs');
import Q = require('q');
import path = require('path');

const app = new Koa();

const readFilePromise = Q.nfbind(fs.readFile);  // 將Callback版本的readFile轉為Promise版本

app.use(async (ctx) => {
    const data = await readFilePromise(path.join(__dirname, 'demo.html'), 'utf8');  // 使用await等待readFilePromise回傳回來的資料
    ctx.body = data;
});

app.listen(3000);

不使用Async function

    
// non-async.ts
import express = require('express');
import fs = require('fs');
import path = require('path');

const app = express();

app.get('/', (req, res) => {
    fs.readFile(path.join(__dirname, 'demo.html'), 'utf8', (err, data) => {
        res.send(data);
    });
});

app.listen(3000);

可以發現的是使用了async後,減少了callback使用的次數,因而降低了callback hell所帶來的危害。

Middleware中間件

中間件是Http server一個很重要的概念,他能讓我們有序列的處理一個http request,讓我們更容易做到模組化。

可以想象處理http request時就是將我們的request丟到一條流水線中,中間件就是流水線上的工人,當request到他面前時他可以經過一些處理再傳給下一個工人,直到產線最後這個request才算處理完畢。

在Koa中,我們使用use這個實例函數來串接起我們的中間件們,use的傳入值是一個callback,而這個callback中有一個context變數,以及一個可選的next函數。

因此我們思考一下下面這個範例

    
// middleware.ts
import Koa = require('koa');

const app = new Koa();

// Stage 1
app.use(async (ctx, next) => {
    ctx.body = 'Stage 1 Pass\n';
    next();                         // 將context轉交給下個middleware
});

// Stage 2
app.use(async (ctx) => {
    ctx.body += 'Stage 2 Pass\n';   // 我們不呼叫next(),因此這是這是最後一個middleware
});

// Stage 3
app.use(async (ctx) => {
    ctx.body += 'Stage 3 Pass\n';   // 因為Stage 2沒有呼叫next(),因此這個中間件不會被執行
});

app.listen(3000);

前端的輸出結果是

Stage 1 Pass
Stage 2 Pass

由此可知,這個request的執行流程為 Image

Koa受惠于這個機制,所以即使Koa kernel本身的功能雖然精簡,但是我們可以透過增加各種不同的中間件來完成許多複雜的任務,接下來的教學中,我們會逐步的介紹加上一些常用的中間件,例如Routing、Views、Session等功能,組合出功能完善的網頁。


References:
1. koa
2. async 函数的含义和用法
3. MDN: Async function
所有範例中的程式碼皆放置在koa-tutorial-sample專案中