理解NodeJS中間件機制核心代碼的實現,加深對中間件機制的理解,有助于更好的使用和編寫中間件。
目錄
- 中間件概念
- 中間件機制核心實現
- 中間件社區
中間件概念
在NodeJS中,中間件主要是指封裝所有Http請求細節處理的方法。一次Http請求通常包含很多工作,如記錄日志、ip過濾、查詢字符串、請求體解析、Cookie處理、權限驗證、參數驗證、異常處理等,但對于Web應用而言,并不希望接觸到這么多細節性的處理,因此引入中間件來簡化和隔離這些基礎設施與業務邏輯之間的細節,讓開發者能夠關注在業務的開發上,以達到提升開發效率的目的。
中間件的行為比較類似Java中過濾器的工作原理,就是在進入具體的業務處理之前,先讓過濾器處理。它的工作模型下圖所示。
中間件機制核心實現
中間件是從Http請求發起到響應結束過程中的處理方法,通常需要對請求和響應進行處理,因此一個基本的中間件的形式如下:
const middleware = (req, res, next) => {
// TODO
next()
}
以下通過兩種方式的中間件機制的實現來理解中間件是如何工作的。
方式一
如下定義三個簡單的中間件:
const middleware1 = (req, res, next) => {
console.log('middleware1 start')
next()
}
const middleware2 = (req, res, next) => {
console.log('middleware2 start')
next()
}
const middleware3 = (req, res, next) => {
console.log('middleware3 start')
next()
}
通過遞歸的形式,將后續中間件的執行方法傳遞給當前中間件,在當前中間件執行結束,通過調用next()
方法執行后續中間件的調用。
// 中間件數組
const middlewares = [middleware1, middleware2, middleware3]
function run (req, res) {
const next = () => {
// 獲取中間件數組中第一個中間件
const middleware = middlewares.shift()
if (middleware) {
middleware(req, res, next)
}
}
next()
}
run() // 模擬一次請求發起
執行以上代碼,可以看到如下結果:
middleware1 start
middleware2 start
middleware3 start
如果中間件中有異步操作,需要在異步操作的流程結束后再調用next()
方法,否則中間件不能按順序執行。改寫middleware2中間件:
const middleware2 = (req, res, next) => {
console.log('middleware2 start')
new Promise(resolve => {
setTimeout(() => resolve(), 1000)
}).then(() => {
next()
})
}
執行結果與之前一致,不過middleware3會在middleware2異步完成后執行。
方式二
有些中間件不止需要在業務處理前執行,還需要在業務處理后執行,比如統計時間的日志中間件。在方式一情況下,無法在next()
為異步操作時再將當前中間件的其他代碼作為回調執行。因此可以將next()
方法的后續操作封裝成一個Promise
對象,中間件內部就可以使用next.then()
形式完成業務處理結束后的回調。改寫run()
方法如下:
function run (req, res) {
const next = () => {
const middleware = middlewares.shift()
if (middleware) {
// 將middleware(req, res, next)包裝為Promise對象
return Promise.resolve(middleware(req, res, next))
}
}
next()
}
中間件的調用方式需改寫為:
const middleware1 = (req, res, next) => {
console.log('middleware1 start')
// 所有的中間件都應返回一個Promise對象
// Promise.resolve()方法接收中間件返回的Promise對象,供下層中間件異步控制
return next().then(() => {
console.log('middleware1 end')
})
}
得益于async
函數的自動異步流程控制,中間件也可以用如下方式來實現:
// async函數自動返回Promise對象
const middleware2 = async (req, res, next) => {
console.log('middleware2 start')
await new Promise(resolve => {
setTimeout(() => resolve(), 1000)
})
await next()
console.log('middleware2 end')
}
const middleware3 = async (req, res, next) => {
console.log('middleware3 start')
await next()
console.log('middleware3 end')
}
執行結果如下:
以上描述了中間件機制中多個異步中間件的調用流程,實際中間件機制的實現還需要考慮異常處理、路由等。
在express
框架中,中間件的實現方式為方式一,并且全局中間件和內置路由中間件中根據請求路徑定義的中間件共同作用,不過無法在業務處理結束后再調用當前中間件中的代碼。koa2
框架中中間件的實現方式為方式二,將next()
方法返回值封裝成一個Promise
,便于后續中間件的異步流程控制,實現了koa2
框架提出的洋蔥圈模型,即每一層中間件相當于一個球面,當貫穿整個模型時,實際上每一個球面會穿透兩次。
koa2
框架的中間件機制實現得非常簡潔和優雅,這里學習一下框架中組合多個中間件的核心代碼。
function compose (middleware) {
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
return function (context, next) {
let index = -1
return dispatch(0)
function dispatch (i) {
// index會在next()方法調用后累加,防止next()方法重復調用
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
// 核心代碼
// 包裝next()方法返回值為Promise對象
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
// 遇到異常中斷后續中間件的調用
return Promise.reject(err)
}
}
}
}
中間件社區
在后續NodeJS學習和應用中,建議使用koa2
框架作為基礎框架,這里列出了一些使用比較多的中間件。
- koa-router:路由中間件
- koa-bodyparser:http請求主體解析
- koa-static:代理靜態文件
- koa-compress:gzip壓縮
- koa-logger:日志記錄
- koa-convert:轉換koa1.x版本的中間件
- kcors:跨域中間件
koa中間件列表地址:https://github.com/koajs/koa/wiki
中間件的具體使用方式還請小伙伴們自行查詢官方文檔。
總結
本文主要介紹了中間件的概念、為何引入中間件以及中間件機制的核心實現。中間件機制使得Web應用具備良好的可擴展性和組合性。
在實現中間件時,單個中間件應該足夠簡單,職責單一。由于每個請求都會調用中間件相關代碼,中間件的代碼應該高效,必要的時候可以緩存重復獲取的數據。在對不同的路由使用中間件時,還應該考慮到不同的中間件應用到不同的路由上。
本文參考資源如下:
- 深入淺出Node.js#8.4
- https://github.com/koajs/koa
- https://cnodejs.org/topic/59a90638ea0aea6b0c64e6ed
- https://cnodejs.org/topic/58fd8ec7523b9d0956dad945
- https://hijiangtao.github.io/2017/11/10/Mastering-Koa-Middleware/
- http://www.lxweimin.com/p/c2b61e37988b
- https://eggjs.org/zh-cn/intro/egg-and-koa.html#midlleware