20行代碼簡(jiǎn)單實(shí)現(xiàn)koa洋蔥圈模型示例詳解
引言
koa想必很多人直接或間接的都用過(guò),其源碼不知道閱讀本文的你有沒(méi)有看過(guò),相當(dāng)精煉,本文想具體說(shuō)說(shuō)koa的中間件模型,一起看看koa-compose的源碼,這也是koa系列的第一篇文章,后續(xù)會(huì)更新一下koa相關(guān)的其他知識(shí)點(diǎn)
koa中間件的使用
先讓我們啟動(dòng)一個(gè)koa服務(wù)
// app.js
const koa = require('koa');
const app = new koa();
app.use(async (ctx, next) => {
console.log('進(jìn)入第一個(gè)中間件')
next();
console.log('退出第一個(gè)中間件')
})
app.use(async (ctx, next) => {
console.log('進(jìn)入第2個(gè)中間件')
next();
console.log('退出第2個(gè)中間件')
})
app.use((ctx, next) => {
console.log('進(jìn)入第3個(gè)中間件')
next();
console.log('退出第3個(gè)中間件')
})
app.use(ctx => {
ctx.body = 'hello koa'
})
app.listen(8080, () => {
console.log('服務(wù)啟動(dòng),監(jiān)聽(tīng)8080端口')
})
上述的服務(wù)在訪(fǎng)問(wèn)的時(shí)候會(huì)得到如下結(jié)果:
服務(wù)啟動(dòng),監(jiān)聽(tīng)8080端口
進(jìn)入第1個(gè)中間件
進(jìn)入第2個(gè)中間件
進(jìn)入第3個(gè)中間件
退出第3個(gè)中間件
退出第2個(gè)中間件
退出第1個(gè)中間件
上面的返回結(jié)果有點(diǎn)像一個(gè)遞歸的過(guò)程,從現(xiàn)象上看當(dāng)中間件調(diào)用next()的時(shí)候,函數(shù)會(huì)暫停并進(jìn)入到下一個(gè)中間件,當(dāng)執(zhí)行了最后一個(gè)中間件后,執(zhí)行代碼會(huì)回溯上游中間件,并執(zhí)行next()之后的代碼,這就是koa的核心能力,洋蔥圈模型
洋蔥圈模型
先看一張經(jīng)典的洋蔥圈模型的示意圖:

在開(kāi)發(fā)過(guò)程中,可以將next()之前的代碼理解為捕獲階段, 而next()之后的代碼可以理解為釋放階段,開(kāi)發(fā)者結(jié)合這兩個(gè)狀態(tài)可以實(shí)現(xiàn)一些有趣的操作,比如記錄一次請(qǐng)求的時(shí)間:
async function responseTime(ctx, next) {
const start = Date.now();
await next();
const ms = Date.now() - start;
ctx.set('X-Response-Time', `${ms}ms`);
}
app.use(responseTime);
使用洋蔥圈模型可以直接將響應(yīng)時(shí)間記錄的操作解耦出來(lái),這樣就不需要再去對(duì)稱(chēng)的寫(xiě)在業(yè)務(wù)邏輯中了,這是怎么實(shí)現(xiàn)的呢?
洋蔥圈模型的實(shí)現(xiàn),koa-compose
我們通過(guò)app.use()添加了很多函數(shù),這些函數(shù)最終傳遞給了compose函數(shù),先貼上koa-compose的源碼,這里筆者刪除掉了校驗(yàn)入?yún)⒌囊恍┓侵鞲蛇壿嫞覀兛梢钥吹酱a也就十幾行,非常簡(jiǎn)單,接下來(lái)讓我們一行一行的去看一下代碼
function compose (middleware) {
// 返回一個(gè)匿名函數(shù),next為可選參數(shù)
return function (context, next) {
// 記錄當(dāng)前執(zhí)行位置的游標(biāo)
let index = -1
// 從第一個(gè)中間件開(kāi)始,串起所有中間件
return dispatch(0)
function dispatch (i) {
// 為了不破壞洋蔥圈模型,不允許在單個(gè)中間件中執(zhí)行多次next函數(shù)
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
// 更新游標(biāo)
index = i
let fn = middleware[i]
// 判斷邊界,假如已經(jīng)到到邊界了,可執(zhí)行外部傳入的回調(diào)
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
// 核心處理邏輯,進(jìn)入fn的執(zhí)行上下文的時(shí)候,dispatch就是通過(guò)綁定下一個(gè)index,變成了next,進(jìn)入到下一個(gè)中間件
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
} catch (err) {
return Promise.reject(err)
}
}
}
}
compose接收一個(gè)的函數(shù)數(shù)組[fn1, fn2, fn3, ...],compose調(diào)用后,返回了一個(gè)匿名函數(shù),匿名函數(shù)接收兩個(gè)參數(shù)
- 第一個(gè)參數(shù)是上下文,對(duì)于koa的上下文不在本文的探究范圍,我們只要記得這個(gè)是在各個(gè)中間件中的共享的就可以了
- 第二個(gè)參數(shù)標(biāo)記為next,可選參數(shù),在中間件執(zhí)行的最后檢查執(zhí)行,這個(gè)和自定義的中間件中的next不完全一樣,一般是初始化返回了匿名函數(shù)后,調(diào)用方自己指定,用于處理一下默認(rèn)邏輯
通過(guò)源碼可以看出,compose是提供了一個(gè)洋蔥模型完全執(zhí)行的回調(diào),通過(guò)把函數(shù)存儲(chǔ)起來(lái),用next作為鑰匙,串起了我們所有的中間件,其核心代碼就是:
Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
進(jìn)入fn的執(zhí)行上下文的時(shí)候,dispatch就是通過(guò)綁定下一個(gè)index,變成了next,進(jìn)入到下一個(gè)中間件。另外fn(中間件函數(shù))可以是個(gè)異步函數(shù),Promise.resolve會(huì)等到內(nèi)部異步函數(shù)resolve之后觸發(fā)
單次調(diào)用限制
假如在單個(gè)中間件中執(zhí)行多次next函數(shù)的話(huà),會(huì)造成下游的中間件多次執(zhí)行,這樣就破壞了洋蔥圈模型,因此限制了在單個(gè)中間件中只能執(zhí)行一次next函數(shù),實(shí)現(xiàn)方式時(shí)在函數(shù)記錄了一個(gè)游標(biāo)index,初始值是-1;這個(gè)游標(biāo)會(huì)記錄當(dāng)前執(zhí)行到哪個(gè)中間件,用來(lái)禁止在中間件中多次調(diào)用next函數(shù)
在一個(gè)中間件內(nèi)多次調(diào)用next的時(shí)候,你就會(huì)收到下面這個(gè)報(bào)錯(cuò)
UnhandledPromiseRejectionWarning: Error: next() called multiple times
koa-compose與流程引擎
koa-compose不僅僅只是koa的一個(gè)依賴(lài)包,在有些場(chǎng)景下完全可以作為一個(gè)獨(dú)立的工具來(lái)使用的,這里模擬一個(gè)代碼檢測(cè)工具的應(yīng)用,完全可以作為一個(gè)流程引擎來(lái)使用
const koaCompose = require('koa-compose');
function download = (ctx, next) {
console.log('download code');
next();
}
function check = (ctx, next) {
console.log('check style');
next();
}
function post = (ctx, next) {
next();
console.log('post result', ctx.result);
}
function clean = (ctx, next) {
next();
console.log('clean temp, remove code');
}
const flowEngine = koaCompose([download, check, post, clean]);
flowEngine(ctx as Context);
上述可以看作一個(gè)基于koa-compose實(shí)現(xiàn)的流程引擎,在node中,我們會(huì)經(jīng)常處理一些多階段的任務(wù),完全可以通過(guò)這樣的方式來(lái)實(shí)現(xiàn)
總結(jié)
koa的洋蔥圈模型在面試中經(jīng)常會(huì)被問(wèn)到,建議可以寫(xiě)一下、理解一下koa-compose的源碼;另外koa-compose作為一個(gè)流程引擎也是一個(gè)很有用的工具,在有些場(chǎng)景下會(huì)有意想不到的效果。
以上就是20行代碼簡(jiǎn)單實(shí)現(xiàn)koa洋蔥圈模型示例詳解的詳細(xì)內(nèi)容,更多關(guān)于koa洋蔥圈模型的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Node.js?子線(xiàn)程Crash?問(wèn)題的排查方法
這篇文章主要介紹了Node.js?子線(xiàn)程Crash?問(wèn)題的排查,本文通過(guò)代碼例子給大家詳細(xì)講解,對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-06-06
Node如何后臺(tái)數(shù)據(jù)庫(kù)使用增刪改查功能
這篇文章主要介紹了Node如何后臺(tái)數(shù)據(jù)庫(kù)使用增刪改查功能,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-11-11
node 使用 nodemailer工具發(fā)送驗(yàn)證碼到郵箱
最近閑著沒(méi)事,我就在練習(xí)使用node和mysql編寫(xiě)接口,計(jì)劃寫(xiě)一個(gè)完整的vue系統(tǒng),這篇文章主要介紹了node 使用 nodemailer工具發(fā)送驗(yàn)證碼到郵箱,需要的朋友可以參考下2023-10-10
詳解如何利用Nodejs構(gòu)建多進(jìn)程應(yīng)用
這篇文章主要為大家介紹了如何利用Nodejs構(gòu)建多進(jìn)程應(yīng)用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10
Node.js中創(chuàng)建和管理外部進(jìn)程詳解
這篇文章主要介紹了Node.js中創(chuàng)建和管理外部進(jìn)程詳解,本文講解了執(zhí)行外部命令的方法、子進(jìn)程相關(guān)內(nèi)容等,需要的朋友可以參考下2014-08-08
node.js實(shí)現(xiàn)學(xué)生檔案管理
這篇文章主要為大家詳細(xì)介紹了node.js實(shí)現(xiàn)學(xué)生檔案管理,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-05-05
node.js中的buffer.Buffer.isEncoding方法使用說(shuō)明
這篇文章主要介紹了node.js中的buffer.Buffer.isEncoding方法使用說(shuō)明,本文介紹了buffer.Buffer.isEncoding的方法說(shuō)明、語(yǔ)法、接收參數(shù)、使用實(shí)例和實(shí)現(xiàn)源碼,需要的朋友可以參考下2014-12-12
Nodejs對(duì)postgresql基本操作的封裝方法
今天小編就為大家分享一篇Nodejs對(duì)postgresql基本操作的封裝方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-02-02
node.js實(shí)現(xiàn)回調(diào)的方法示例
這篇文章主要介紹了node.js實(shí)現(xiàn)回調(diào)的方法,結(jié)合實(shí)例形式分析了node.js實(shí)現(xiàn)向回調(diào)函數(shù)傳遞參數(shù)、閉包的使用及鏈?zhǔn)交卣{(diào)相關(guān)操作技巧,需要的朋友可以參考下2017-03-03

