(Middleware),又稱中介層,是提供系統軟件和應用軟件之間連接的軟件,以便於軟件各部件之間的溝通,特別是應用軟件對於系統軟件的集中的邏輯。中間件在企業架構中表示各種軟件套件,有助於抽象底層機制,比如操作系統 API、網絡通信、內存管理等,開發者只需要關注應用中的業務模塊。
從更廣義的角度來看,中間件也可以定義爲鏈接底層服務和應用的軟件層。後文我們主要使用 Node.js 裏最近很熱門的框架 Koa2 裏的中間件概念爲例,並且自己實現一箇中間件來加深理解。
1. 什麼是中間件
在 Express、Koa2 中,中間件代表一系列以管道形式被連接起來,以處理 HTTP 請求和響應的函數。換句話說,中間件其實就是一個函數,一個執行特定邏輯的函數。前端中類似的概念還有攔截器、Vue 中的過濾器、vue-router 中的路由守衛等。
工作原理就是進入具體業務之前,先對其進行預處理(在這一點上有點類似於裝飾器模式),或者在進行業務之後,對其進行後處理。
示意圖如下:
當接受到一個請求,對這個請求的處理過程可以看作是一個串聯的管道,比如對於每個請求,我們都想插入一些相同的邏輯比如權限驗證、數據過濾、日誌統計、參數驗證、異常處理等功能。對於開發者而言,自然不希望對於每個請求都特殊處理,因此引入中間件來簡化和隔離這些基礎設施與業務邏輯之間的細節,讓開發者能夠關注在業務的開發上,以達到提升開發效率的目的。
2. Koa 裏的中間件
2.1. Koa2 裏的中間件使用
Koa2 中的中間件形式爲:
app.use(async function middleware(context, next){
// ... 前處理
await next() // 下一個中間件
// ... 後處理
})
其中第一個參數 context作爲上下文封裝了request 和 response 信息,我們可以通過它來訪問request 和 response;next 是下一個中間件,當一箇中間件處理完畢,調用 next() 就可以執行下一個中間件,下一個中間件處理完再使用 next(),從而實現中間件的管道化,對消息的依次處理。
一般中間件模式都約定有個 use 方法來註冊中間件,Koa2 也是如此。千言萬語不及一行代碼,這裏寫一個簡單的中間件:
const koa = require('koa')
const app = new koa()
// 沒錯,這就是中間件
app.use((ctx, next) => {
console.log('in 中間件1')
})
app.listen(10001)
// in 中間件1
Koa2 中的中間件有多種類型:
1. 應用級中間件;
2. 路由級中間件;
3. 錯誤處理中間件;
4. 第三方中間件;
除了使用第三方中間件比如 koa-router、koa-bodyparser、koa-static、koa-logger 等提供一些通用的路由、序列化、反序列化、日誌記錄等功能外,我們還可以編寫自己的應用級中間件,來完成業務相關的邏輯。
1. request和 response 的解析和處理;
2. 生成訪問日誌;
3. 管理 session、cookie 等;
4. 提供網絡安全防護;
2.2. 洋蔥模型
在使用多箇中間件時,引用一張著名的洋蔥模型圖:
正如上面的洋蔥圖所示,請求在進入業務邏輯時,會依次經過一系列中間件,對數據進行有序處理,業務邏輯之後,又像棧的先入後出一樣,倒序經過之前的中間件。洋蔥模型允許當應用執行完主要邏輯之後進行一些後處理,再將響應返回給用戶。
使用如下:
const Koa = require('koa')
const app = new Koa()
// 中間件1
app.use(async (ctx, next) => {
console.log('in 中間件1')
await next()
console.log('out 中間件1')
})
// 中間件2
app.use(async (ctx, next) => {
console.log('in 中間件2')
await next()
console.log('out 中間件2')
})
// response
app.use(async ctx => { ctx.body = 'Hello World' })
app.listen(10001)
console.log('app started at port http://localhost:10001')
// in 中間件1
// in 中間件2
// out 中間件2
// out 中間件1
我們可以引入 setTimeout 來模擬異步請求的過程:
const Koa = require('koa')
const app = new Koa()
// 中間件1
app.use(async (ctx, next) => {
console.log('in 中間件1')
await next()
console.log('out 中間件1')
})
// 中間件2
app.use(async (ctx, next) => {
console.log('in 中間件2')
await new Promise((resolve, reject) => {
ctx.zjj_start2 = Date.now()
setTimeout(() => resolve(), 1000 + Math.random() * 1000)
}
)
await next()
const duration = Date.now() - ctx.zjj_start2
console.log('out 中間件2 耗時:' + duration + 'ms')
})
// 中間件3
app.use(async (ctx, next) => {
console.log('in 中間件3')
await new Promise((resolve, reject) => {
ctx.zjj_start3 = Date.now()
setTimeout(() => resolve(), 1000 + Math.random() * 1000)
}
)
await next()
const duration = Date.now() - ctx.zjj_start3
console.log('out 中間件3 耗時:' + duration + 'ms')
})
// response
app.use(async ctx => {
console.log(' ... 業務邏輯處理過程 ... ')
})
app.listen(10001)
console.log('app started at port http://localhost:10001')
效果如下:
在使用多箇中間件時,特別是存在異步的場景,一般要 await來調用 next來保證在異步場景中,中間件仍按照洋蔥模型的順序來執行,因此別忘了 next 也要通過 await 調用。
更多關於Koa的使用,可以瀏覽 Koa與常用中間件的使用 這篇文章
參考文檔:
1. Koa中文文檔