Koa 是一個由 Express 原班人馬打造的新的 web 框架,Koa 本身並沒有捆綁任何中間件,只提供了應用(Application)、上下文(Context)、請求(Request)、響應(Response)四個模塊。原本 Express 中的路由(Router)模塊已經被移除,改為通過中間件的方式實現。相比較 Express,Koa 能讓使用者更大程度上構建個性化的應用。
一、中間件簡介
Koa 是一個中間件框架,本身沒有捆綁任何中間件。本身支持的功能並不多,功能都可以通過中間件拓展實現。通過添加不同的中間件,實現不同的需求,從而構建一個 Koa 應用。
Koa 的中間件就是函數,可以是 async 函數,或是普通函數,以下是官網的示例:
// async 函數
app.use(async (ctx, next) => { const start = Date.now(); await next(); const ms = Date.now() - start; console.log(`${ctx.method} ${ctx.url} - ${ms}ms`); }); // 普通函數
app.use((ctx, next) => { const start = Date.now(); return next().then(() => { const ms = Date.now() - start; console.log(`${ctx.method} ${ctx.url} - ${ms}ms`); }); });
中間件可以通過官方維護的倉庫查找獲取,也可以根據需求編寫屬於自己的中間件。
二、中間件原理
下面是一個的 Koa 應用,簡單演示了中間件的執行順序:
const Koa = require('Koa'); const app = new Koa(); // 最外層的中間件
app.use(async (ctx, next) => { await console.log(`第 1 個執行`); await next(); await console.log(`第 8 個執行`); }); // 第二層中間件
app.use(async (ctx, next) => { await console.log(`第 2 個執行`); await console.log(`第 3 個執行`); await next(); await console.log(`第 6 個執行`); await console.log(`第 7 個執行`); }); // 最里層的中間件
app.use(async (ctx, next) => { await console.log(`第 4 個執行`); ctx.body = "Hello world."; await console.log(`第 5 個執行`); }); app.listen(3000, () => { console.log(`Server port is 3000.`); })
原理:從上面的示例中可以看出,中間件的執行順序並不是從頭到尾,而是類似於前端的事件流。事件流是先進行事件捕獲,到達目標,然后進行事件冒泡。中間件的實現過程也是一樣的,先從最外面的中間件開始執行,next()
后進入下一個中間件,一路執行到最里面的中間件,然后再從最里面的中間件開始往外執行。
Koa 中間件采用的是洋蔥圈模型,每次執行下一個中間件傳入兩個參數 ctx 和 next,參數 ctx 是由 koa 傳入的封裝了 request 和 response 的變量,可以通過它訪問 request 和 response,next 就是進入下一個要執行的中間件。
三、編寫屬於自己的中間件
中間件有兩個重點:
(1)調用 app.use() 來應用一個中間件;
(2)調用 next() 繼續執行下一個中間件(可能不存在更多的中間件,只是讓執行繼續下去)。
1、token 驗證的 middleware
前后端分離開發,我們常采用 JWT 來進行身份驗證,其中 token 一般放在 HTTP 請求中的 Header Authorization 字段中,每次請求后端都要進行校驗,如 Java 的 Spring 框架可以在過濾器中對 token 進行統一驗證,而 Koa 則通過編寫中間件來實現 token 驗證。
// token.js // token 中間件
module.exports = (options) => async (ctx, next) { try { // 獲取 token
const token = ctx.header.authorization if (token) { try { // verify 函數驗證 token,並獲取用戶相關信息
await verify(token) } catch (err) { console.log(err) } } // 進入下一個中間件
await next() } catch (err) { console.log(err) } }
// app.js // 引入 token 中間件
const Koa = require('Koa'); const app = new Koa(); const token = require('./token') app.use(token()) app.listen(3000, () => { console.log(`Server port is 3000.`); })
2、log 的 middleware
日志模塊也是線上不可缺少的一部分,完善的日志系統可以幫助我們迅速地排查出線上的問題。通過 Koa 中間件,我們可以實現屬於自己的日志模塊
// logger.js // logger 中間件
const fs = require('fs') module.exports = (options) => async (ctx, next) => { const startTime = Date.now() const requestTime = new Date() await next() const ms = Date.now() - startTime; let logout = `${ctx.request.ip} -- ${requestTime} -- ${ctx.method} -- ${ctx.url} -- ${ms}ms`; // 輸出日志文件
fs.appendFileSync('./log.txt', logout + '\n') }
// app.js // 引入 logger 中間件
...const logger = require('./logger') app.use(logger()) ... app.listen(3000, () => { console.log(`Server port is 3000.`); })
可以結合 log4js 等包來記錄更詳細的日志。
3、返回字段由下划線改為駝峰的中間件
// toHump.js
const toHump = async (ctx, next) => { ctx.write = (obj) => ctx.body = toHumpFun(obj) await next() } function toHumpFun(obj) { const result = Array.isArray(obj) ? [] : {} for (const key in obj) { if (obj.hasOwnProperty(key)) { const element = obj[key]; const index = key.indexOf('_') let newKey = key if (index === -1 || key.length === 1) { result[key] = element } else { const keyArr = key.split('_') const newKeyArr = keyArr.map((item, index) => { if (index === 0) return item return item.charAt(0).toLocaleUpperCase() + item.slice(1) }) newKey = newKeyArr.join('') result[newKey] = element } if (typeof element === 'object' && element !== null) { result[newKey] = toHumpFun(element) } } } return result } module.exports = toHump
// app.js
const toHump = require('./toHump') app.use(toHump) // 需要放在引用路由之前
使用 ctx.write(data)
替換 ctx.body = data
我們已經了解中間件的原理,以及如何實現一個自己的中間件。中間件的代碼通常比較簡單,我們可以通過閱讀官方維護的倉庫中優秀中間件的源碼,來加深對中間件的理解和運用。