人生就是這麼 Koa(2) … QAQ
為什麼要 Koa?
而且還有可怕的 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
| 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() var result = g.next() result.value .then(data => g.next(data)) .catch(err => g.throw(err))
|
拿掉執行的程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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
| 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
| 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
| co(function* () { var result = yield Promise.resolve(true) return result }) .then(value => console.log(value)) .catch(err => console.log(err.stack)) 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()
- 關鍵字
yield
→ await
- 一樣 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
物件底下
- 包含
request
與 response
物件
- 設定了一堆 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
| app.use(async function (ctx, next) { const start = new Date(); await next(); const ms = new Date() - start; ctx.set('X-Response-Time', `${ms}ms`); });
|
- 再講一次,
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
| app.use(async (ctx, next) => { const { token } = ctx.query try { ctx.state.user = JWT.verify(token, 'secret_string') await next() } catch(err) { ctx.throw(401, 'Unauthorized') } }) app.use(async ctx => { const { db } = ctx const { id } = ctx.state.user try { const friends = db.Friend.find(id) ctx.status = 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 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
/ ETag
與 If-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 { await next() } catch(err) { throw err } }) app.use(async ctx => { try { throw new Error('Server Error') } catch(err) { throw err } }) app.on('error', (err, 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 Allowed
與 501 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.js
與 users.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
| const Router = require('koa-router') const router = Router() router .use(someMiddleware) .post('/login', ctx => {...}) .post('/logout', ctx => {...}) module.exports = router const Router = require('koa-router') const router = Router() const auth_mid = require('../middlewares/auth') router .get('/:userID', ctx => { const { userID } = ctx.params }) module.exports = router 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 const Koa = require('koa') const router = require('./routes/index') const app = new Koa() app.use(router.routes()) app.listen(3000)
|
常用的 middleware
使用 compose 合併 middleware
koa-compose 可以將多個 middleware 合併成一個,讓程式碼更加優雅
1 2 3 4 5 6
| const compose = require('koa-compose') app.use(compose([ middleware1, middleware2, ... ]))
|
Reference