人生就是這麼Koa...QAQ

人生就是這麼 Koa(2) … QAQ

為什麼要 Koa?

callback hell

而且還有可怕的 Error Handling …

  • Sync 的 try… catch(err)…
  • fn(err, callback)
  • Promise.then().catch(err)

不同方式產生的 Error,沒有統一的 handle 方法。(對自己好一點,你值得擁有更好 >.0)

快速 Review 一下 Generator

Generator with Promise

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// generator readFile
var Promise = require('bluebird')
var fs = Promise.promisifyAll(require('fs'))
function* gen() {
try {
var data = yield fs.readFileAsync('file1')
console.log('result', data.toString())
} catch(err) {
console.error('居然抓的到!!', err)
}
return
}
// 執行
var g = gen()
// result.value 為 readFileAsync 回傳的 Promise
var result = g.next()
result.value
.then(data => g.next(data)) // 將 result 扔回 generator
.catch(err => g.throw(err))

拿掉執行的程式碼

1
2
3
4
5
6
7
8
9
10
11
12
13
// generator readFile
var Promise = require('bluebird')
var fs = Promise.promisifyAll(require('fs'))
function* gen() {
try {
var data = yield fs.readFileAsync('file1')
console.log('result', data.toString())
} catch(err) {
console.error('居然抓的到!!', err)
}
return
}

等等,不對啊,要是我有很多非同步操作勒?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// multi_async
var Promise = require('bluebird')
var fs = Promise.promisifyAll(require('fs'))
function* gen() {
try {
var data1 = yield fs.readFileAsync('file1')
console.log('result1', data1.toString())
var data2 = yield fs.readFileAsync('file2')
console.log('result2', data2.toString())
var data3 = yield fs.readFileAsync('file3')
console.log('result2', data3.toString())
} catch(err) {
console.error('居然抓的到!!', err)
}
return
}
// 執行
var g = gen()
g.next().value
.then(data1 => g.next(data1).value)
.then(data2 => g.next(data2).value)
.then(data3 => g.next(data3).value)
.catch(err => g.throw(err))

本身執行過程是可以疊代的 (Iterative),因此可以寫成執行器 (executor)

1
2
3
4
5
6
7
8
9
10
11
12
13
// executor
function run(gen){
var g = gen()
function next(data) {
var result = g.next(data)
if (result.done) return result.value
result.value
.then(data => next(data))
.catch(err => g.throw(err))
}
next()
}
run(gen)

co module

  • TJ 大神作品,Generator auto runner
  • co(fn*) 執行 generator 並 return Promise,因此允許 Generator in Generator
  • co.wrap(fn*) 可直接將 generator 轉換成 return Promise 的一般 function
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// use co as auto runner
co(function* () {
var result = yield Promise.resolve(true)
return result
})
.then(value => console.log(value))
.catch(err => console.log(err.stack))
// use co to convert generator function to a regular function
var fn = co.wrap(function* (val) {
return yield Promise.resolve(val)
})
fn(true)
.then(value => console.log(value))
.catch(err => console.log(err.stack))

所以講 Generator 是要幹嘛?

  • 因為 Koa1 就是基於 Generator (雖然我們主題是 Koa2)
  • Koa1 與 Koa2 最大的差異就是 callback function 從 generator function → async/await function

async/await 又是什麼?

  • 沒什麼,就是標準化的 Generator auto runner (ECMAScript 2016/ES7)
  • 然後 function 宣告從 function*()async function()
  • 關鍵字 yieldawait
  • 一樣 return Promise
1
2
3
4
5
6
7
async function fn() {
var result = await Promise.resolve(true)
return result
}
fn()
.then(value => console.log(value))
.catch(err => console.log(err))

Koa - 講這麼多,終於來到了苦啊!

  • 與 Express 概念類似的 node web framework,但是更陽春…
  • Koa 相較於 express 的 callback 都是 regular function,其 callback function 皆是 async function,並自動幫你 handle 執行

當然如果你只想要在 Express 的 regular function 中,透過再宣告 async function 享受 async/await 語法,那就不用往下看了…

Express

1
2
3
4
5
6
const express = require('express')
const app = express()
app.get('/', function (req, res) {
res.send('Hello World')
})
app.listen(3000)

Koa

1
2
3
4
5
6
7
8
const Koa = require('koa')
const app = new Koa()
app.use(async (ctx, next) => {
if (ctx.method === 'GET' && ctx.path === '/') {
return ctx.body = 'Hello World'
}
})
app.listen(3000)

特色

  • 真的非常陽春,連 router 都沒有
    • 不過社群有各種功能的 middleware 的實作可套用
  • 將所有的物件與操作皆封裝至 ctx 物件底下
    • 包含 requestresponse 物件
    • 設定了一堆 aliases 方便取用
  • middleware 包含 downstream 與 upstream
  • 基本的 Error Handling 處理
    • 404 以及不會洩漏 server stack error
  • 一切都是 async function,開心的同步思維與 try… catch(err)… 吧!

app

同一個 Koa 應用不只能掛載到一個 HTTP Server

app.listen(port)

  • 在特定 port 啟動監聽
  • http.createServer(app.callback()).listen(port) 等價

app.callback()

  • 返回可被 node HTTP server 接受的 callback function (requestListener)
  • requestListener: function(req, res) {}
1
2
3
4
5
const http = require('http');
const Koa = require('koa');
const app = new Koa();
http.createServer(app.callback()).listen(3000);
http.createServer(app.callback()).listen(3001);

app.use(async (ctx, next) => {})

  • 套用 middleware
  • await next() 之前為 downstream;await next() 之後為 upstream
  • 下節再詳述…

app.context

  • 在 app 之間用於共享,類似 global value 的物件
    1
    2
    3
    4
    app.context.db = db();
    app.use(async (ctx) => {
    console.log(ctx.db);
    });

Middleware

1
2
3
4
5
6
7
8
9
10
11
12
// x-response-time:計算每次請求到返回間的運行時間
// 這在 express 中非常難達成
app.use(async function (ctx, next) {
// downstream start
const start = new Date();
// downstream end
await next(); // 往下層 middlware 的 downstream 執行
// upstream start
const ms = new Date() - start;
ctx.set('X-Response-Time', `${ms}ms`);
// upstream end
});
  • 再講一次,await next() 之前為 downstream;await next() 之後為 upstream
  • 每層 middleware 遇到 await next() 之後,會暫停執行,往下層 middleware 的 downstream
  • 直到最底層 middleware 的 downstream 執行完後,以倒序執行 upstream 的程式碼
    • 下層 upstream 執行完後,繼續執行上一層的 upstream

直接拿官方現成的教學,還是 koa1 的範例…

Simple Demo Code

Warning: the demo cannot work

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// token verify
app.use(async (ctx, next) => {
// get by query string
const { token } = ctx.query
// get by cookies
// const token_in_cookies = ctx.cookies.get('token', {httpOnly: true})
// get by headers
// const { authorization } = ctx.headers
// get by request body (need koa-bodyparser middleware)
// const { token } = ctx.request.body
try {
ctx.state.user = JWT.verify(token, 'secret_string')
await next()
} catch(err) {
ctx.throw(401, 'Unauthorized')
}
})
// find friends of the users
app.use(async ctx => {
const { db } = ctx
const { id } = ctx.state.user
try {
const friends = db.Friend.find(id)
ctx.status = 200 // 預設 response status code = 200
ctx.set('ETag', 'a-unique-hash-id')
ctx.body = {
code: 200,
status: 'success'
data: friends,
}
} catch(err) {
let err = new Error('db error')
err.status = 500
err.expose = true // 不會洩漏 server stack 給 client
throw err
}
})

Context, Request 與 Response 的用法還是去看官方文件最快,後面只提出幾個比較特別的。

Context

ctx.state

  • 用於傳遞暫存資料給不同 middlewares
  • e.g. ctx.state.user = await User.find(id)

Request

request.fresh (=ctx.fresh)

  • 自動幫你透過 If-None-Match / ETagIf-Modified-Since / Last-Modified 比較快取是否過期

Content Negotiation

Koa 的 request 物件可幫你設定一些請求的接受型態

  • request.accepts(types)
  • request.acceptsEncodings(types)
  • request.acceptsCharsets(charsets)
  • request.acceptsLanguages(langs)

Response

response.body (=ctx.body)

非常強大的 response 方式,無需根據返回類型選擇不同的回應方法,一律使用 response.body = xxx

  • 接受 string, Buffer, Stream, Object || Array, null 類型
    • Object || Array 為 JSON.stringify 後的結果

Error Handling

  • 在 Koa 的錯誤處理非常單純,直接 try… catch(err)…,然後拋出 error 即可
    • 注意:拋出的 error,一樣會走完每層 upstream,所以可以在每層 middleware 中 catch 到底層丟上來的錯誤,因此非必要,不要修改 error,直接往上拋即可,否則取得的 error 可能不是預期的
  • Koa 預設的錯誤處理的行為是會將除了 404 與 err.expose=true 以外的錯誤全輸出至 stderr
  • 可透過 app.on('error', cb(err, ctx)) 覆寫預設的錯誤處理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
app.use(async (ctx, next) => {
try {
// do nothing, just pass
await next()
} catch(err) {
// get a error from the deeper middleware
// don't modify, just throw up
throw err // err = Error('Server Error')
}
})
app.use(async ctx => {
try {
throw new Error('Server Error')
} catch(err) {
throw err
}
})
app.on('error', (err, ctx) => {
// 覆寫預設錯誤處理的行為
// 若是發生的錯誤會導致 Koa 連 response 也無法,則會額外傳遞錯誤發生時的 ctx
console.error('error', err.stack)
})

Router

Koa 的第三方套件有許多套實作,這邊介紹最流行的 koa-router

特色

  • Express-style routing
    • e.g. app.get, app.put, app.post
  • callback 可吃 regular function, generator function 與 async function
  • 根據定義的 routing,自動提供 OPTIONS 方法
    • 並提供自動回應 405 Method Not Allowed501 Not Implemented 的功能
  • router 可巢狀定義再合併

基本用法

1
2
3
4
5
6
7
8
9
const app = require('koa')()
const router = require('koa-router')()
router.get('/', async (ctx, next) => {...})
router.post('/:variable', async (ctx, next) => {...})
app
.use(router.routes())
.use(router.allowedMethods())
  • router.get|put|post|patch|delete(path, ...middleware):routing 定義
  • router.routes():返回全部定義的 routing 的 middleware
  • router.allowedMethods():返回允許的 OPTIONS 的 middleware,內容為所有定義的 routing 的方法
  • router.use([path], ...middleware):可根據 routing 套用多個 middleware
  • router.param(param, middleware):若定義的 routing 有符合 param 可在進入 routing 前,透過 middleware 預先處理這個 param
1
2
3
4
5
6
7
8
router
.param('user', (id, ctx, next) => {
ctx.user = users[id]
if (!ctx.user) return ctx.status = 404
return next()
})
.get('/users/:user', async ctx => ctx.body = ctx.user)
.get('/users/:user/friends', async ctx => ctx.body = await ctx.user.getFriends())

複雜的巢狀例子

假設現在有 /auth/users 兩支 routing,分別定義在 auth.jsusers.js,並透過 routes/index.js 合併 routes。在 Koa 中直接引用 routes/index.js 匯出的 route。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// auth.js
const Router = require('koa-router')
const router = Router()
router
.use(someMiddleware)
.post('/login', ctx => {...})
.post('/logout', ctx => {...})
module.exports = router
// users.js
const Router = require('koa-router')
const router = Router()
const auth_mid = require('../middlewares/auth')
router
.get('/:userID', ctx => {
const { userID } = ctx.params // 透過 ctx.params 存取 route url 變數
// ctx.body = ...
})
module.exports = router
// routes/index.js (routes)
const Router = require('koa-router')
const router = Router()
const auth_routes = require('./auth')
const users_routes = require('./users')
router
.use('/auth', auth_routes.routes(), auth_routes.allowedMethods())
.use('/users', users_routes.routes(), users_routes.allowedMethods())
module.exports = router
// index.js (koa app)
const Koa = require('koa')
const router = require('./routes/index')
const app = new Koa()
app.use(router.routes())
app.listen(3000)
// 實際上使用時的 router
// POST -> /auth/login
// POST -> /auth/logout
// GET -> /users/{userID}

常用的 middleware

  • koa-bodyparser:parsing body request
  • koa-logger:會有 request/response 的資訊輸出
  • kcors:設定 cross domain access
  • koa-json:讓 json response 的 router 帶有 pretty query string,可美化 json response
  • koa-static:指定資料夾輸出靜態檔案
  • koa-session:session in Koa

使用 compose 合併 middleware

koa-compose 可以將多個 middleware 合併成一個,讓程式碼更加優雅

1
2
3
4
5
6
const compose = require('koa-compose')
app.use(compose([
middleware1,
middleware2,
...
]))

Simple JWT Demo with Koa2

Reference

Share