摘要:原文博客地址,歡迎學(xué)習(xí)交流點擊預(yù)覽讀了下的源碼,寫的相當(dāng)?shù)木啠龅教幚碇虚g件執(zhí)行的模塊決定學(xué)習(xí)一下這個模塊的源碼。當(dāng)在下游沒有更多的中間件執(zhí)行后,堆棧將展開并且每個中間件恢復(fù)執(zhí)行其上游行為。
原文博客地址,歡迎學(xué)習(xí)交流:點擊預(yù)覽
讀了下Koa的源碼,寫的相當(dāng)?shù)木啠龅教幚碇虚g件執(zhí)行的模塊koa-Compose,決定學(xué)習(xí)一下這個模塊的源碼。
閱讀本文可以學(xué)到:
Koa中間件的加載
next參數(shù)的來源
中間件控制權(quán)執(zhí)行順序
先上一段使用Koa啟動服務(wù)的代碼:
放在文件app.js中
const koa = require("koa"); // require引入koa模塊 const app = new koa(); // 創(chuàng)建對象 app.use(async (ctx,next) => { console.log("第一個中間件") next(); }) app.use(async (ctx,next) => { console.log("第二個中間件") next(); }) app.use((ctx,next) => { console.log("第三個中間件") next(); }) app.use(ctx => { console.log("準(zhǔn)備響應(yīng)"); ctx.body = "hello" }) app.listen(3000)
以上代碼,可以使用node app.js啟動,啟動后可以在瀏覽器中訪問http://localhost:3000/
訪問后,會在啟動的命令窗口中打印出如下值:
第一個中間件
第二個中間件
第三個中間件
準(zhǔn)備響應(yīng)
代碼說明:
app.use()方法,用來將中間件添加到隊列中
中間件就是傳給app.use()作為的參數(shù)的函數(shù)
使用app.use()將函數(shù)添加至隊列之中后,當(dāng)有請求時,會依次觸發(fā)隊列中的函數(shù),也就是依次執(zhí)行一個個中間件函數(shù),執(zhí)行順序按照調(diào)用app.use()添加的順序。
在每個中間件函數(shù)中,會執(zhí)行next()函數(shù),意思是把控制權(quán)交到下一個中間件(實際上是調(diào)用next函數(shù)后,會調(diào)用下一個中間件函數(shù),后面解析源碼會有說明),如果不調(diào)用next()函數(shù),不能調(diào)用下一個中間件函數(shù),那么隊列執(zhí)行也就終止了,在上面的代碼中表現(xiàn)就是不能響應(yīng)客戶端的請求了。
app.use(async (ctx,next) => { console.log("第二個中間件") // next(); 注釋之后,下一個中間件函數(shù)就不會執(zhí)行 })內(nèi)部過程分析
內(nèi)部利用app.use()添加到一個數(shù)組隊列中:
// app.use()函數(shù)內(nèi)部添加 this.middleware.push(fn); // 最終this.middleware為: this.middleware = [fn,fn,fn...]
具體參考這里Koa的源碼use函數(shù):https://github.com/koajs/koa/blob/master/lib/application.js#L104
使用koa-compose模塊的compose方法,把這個中間件數(shù)組合并成一個大的中間件函數(shù)
const fn = compose(this.middleware);
具體參考這里Koa的源碼https://github.com/koajs/koa/blob/master/lib/application.js#L126
在有請求后后會執(zhí)行這個中間件函數(shù)fn,進而會把所有的中間件函數(shù)依次執(zhí)行
這樣片面的描述可能會不知所云,可以跳過不看,只是讓諸位知道Koa執(zhí)行中間件的過程
本篇主要是分析koa-compose的源碼,之后分析整個Koa的源碼后會做詳細(xì)說明
所以最主要的還是使用koa-compose模塊來控制中間件的執(zhí)行,那么來一探究竟這個模塊如何進行工作的
koa-composekoa-compose模塊可以將多個中間件函數(shù)合并成一個大的中間件函數(shù),然后調(diào)用這個中間件函數(shù)就可以依次執(zhí)行添加的中間件函數(shù),執(zhí)行一系列的任務(wù)。
源碼地址:https://github.com/koajs/compose/blob/master/index.js
先從一段代碼開始,創(chuàng)建一個compose.js的文件,寫入如下代碼:
const compose = require("koa-compose"); function one(ctx,next){ console.log("第一個"); next(); // 控制權(quán)交到下一個中間件(實際上是可以執(zhí)行下一個函數(shù)), } function two(ctx,next){ console.log("第二個"); next(); } function three(ctx,next){ console.log("第三個"); next(); } // 傳入中間件函數(shù)組成的數(shù)組隊列,合并成一個中間件函數(shù) const middlewares = compose([one, two, three]); // 執(zhí)行中間件函數(shù),函數(shù)執(zhí)行后返回的是Promise對象 middlewares().then(function (){ console.log("隊列執(zhí)行完畢"); })
可以使用node compose.js運行此文件,命令行窗口打印出:
第一個
第二個
第三個
隊列執(zhí)行完畢
中間件這兒的重點,是compose函數(shù)。compose函數(shù)的源代碼雖然很簡潔,但要理解明白著實要下一番功夫。
以下為源碼分析:
"use strict" /** * Expose compositor. */ // 暴露compose函數(shù) module.exports = compose /** * Compose `middleware` returning * a fully valid middleware comprised * of all those which are passed. * * @param {Array} middleware * @return {Function} * @api public */ // compose函數(shù)需要傳入一個數(shù)組隊列 [fn,fn,fn,fn] function compose (middleware) { // 如果傳入的不是數(shù)組,則拋出錯誤 if (!Array.isArray(middleware)) throw new TypeError("Middleware stack must be an array!") // 數(shù)組隊列中有一項不為函數(shù),則拋出錯誤 for (const fn of middleware) { if (typeof fn !== "function") throw new TypeError("Middleware must be composed of functions!") } /** * @param {Object} context * @return {Promise} * @api public */ // compose函數(shù)調(diào)用后,返回的是以下這個匿名函數(shù) // 匿名函數(shù)接收兩個參數(shù),第一個隨便傳入,根據(jù)使用場景決定 // 第一次調(diào)用時候第二個參數(shù)next實際上是一個undefined,因為初次調(diào)用并不需要傳入next參數(shù) // 這個匿名函數(shù)返回一個promise return function (context, next) { // last called middleware # //初始下標(biāo)為-1 let index = -1 return dispatch(0) function dispatch (i) { // 如果傳入i為負(fù)數(shù)且<=-1 返回一個Promise.reject攜帶著錯誤信息 // 所以執(zhí)行兩次next會報出這個錯誤。將狀態(tài)rejected,就是確保在一個中間件中next只調(diào)用一次 if (i <= index) return Promise.reject(new Error("next() called multiple times")) // 執(zhí)行一遍next之后,這個index值將改變 index = i // 根據(jù)下標(biāo)取出一個中間件函數(shù) let fn = middleware[i] // next在這個內(nèi)部中是一個局部變量,值為undefined // 當(dāng)i已經(jīng)是數(shù)組的length了,說明中間件函數(shù)都執(zhí)行結(jié)束,執(zhí)行結(jié)束后把fn設(shè)置為undefined // 問題:本來middleware[i]如果i為length的話取到的值已經(jīng)是undefined了,為什么要重新給fn設(shè)置為undefined呢? if (i === middleware.length) fn = next //如果中間件遍歷到最后了。那么。此時return Promise.resolve()返回一個成功狀態(tài)的promise // 方面之后做調(diào)用then if (!fn) return Promise.resolve() // try catch保證錯誤在Promise的情況下能夠正常被捕獲。 // 調(diào)用后依然返回一個成功的狀態(tài)的Promise對象 // 用Promise包裹中間件,方便await調(diào)用 // 調(diào)用中間件函數(shù),傳入context(根據(jù)場景不同可以傳入不同的值,在KOa傳入的是ctx) // 第二個參數(shù)是一個next函數(shù),可在中間件函數(shù)中調(diào)用這個函數(shù) // 調(diào)用next函數(shù)后,遞歸調(diào)用dispatch函數(shù),目的是執(zhí)行下一個中間件函數(shù) // next函數(shù)在中間件函數(shù)調(diào)用后返回的是一個promise對象 // 讀到這里不得不佩服作者的高明之處。 try { return Promise.resolve(fn(context, function next () { return dispatch(i + 1) })) } catch (err) { return Promise.reject(err) } } } }
補充說明:
根據(jù)以上的源碼分析得到,在一個中間件函數(shù)中不能調(diào)用兩次next(),否則會拋出錯誤
function one(ctx,next){ console.log("第一個"); next(); next(); }
拋出錯誤:
next() called multiple times
next()調(diào)用后返回的是一個Promise對象,可以調(diào)用then函數(shù)
function two(ctx,next){ console.log("第二個"); next().then(function(){ console.log("第二個調(diào)用then后") }); }
中間件函數(shù)可以是async/await函數(shù),在函數(shù)內(nèi)部可以寫任意的異步處理,處理得到結(jié)果后再進行下一個中間件函數(shù)。
創(chuàng)建一個文件問test-async.js,寫入以下代碼:
const compose = require("koa-compose"); // 獲取數(shù)據(jù) const getData = () => new Promise((resolve, reject) => { setTimeout(() => resolve("得到數(shù)據(jù)"), 2000); }); async function one(ctx,next){ console.log("第一個,等待兩秒后再進行下一個中間件"); // 模擬異步讀取數(shù)據(jù)庫數(shù)據(jù) await getData() // 等到獲取數(shù)據(jù)后繼續(xù)執(zhí)行下一個中間件 next() } function two(ctx,next){ console.log("第二個"); next() } function three(ctx,next){ console.log("第三個"); next(); } const middlewares = compose([one, two, three]); middlewares().then(function (){ console.log("隊列執(zhí)行完畢"); })
可以使用node test-async.js運行此文件,命令行窗口打印出:
第一個,等待兩秒后再進行下一個中間件
第二個
第三個
第二個調(diào)用then后
隊列執(zhí)行完畢
在以上打印輸出過程中,執(zhí)行第一個中間件后,在內(nèi)部會有一個異步操作,使用了async/await后得到同步操作一樣的體驗,這步操作可能是讀取數(shù)據(jù)庫數(shù)據(jù)或者讀取文件,讀取數(shù)據(jù)后,調(diào)用next()執(zhí)行下一個中間件。這里模擬式等待2秒后再執(zhí)行下一個中間件。
更多參考了async/await:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/async_function執(zhí)行順序
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/await
調(diào)用next后,執(zhí)行的順序會讓人產(chǎn)生迷惑,創(chuàng)建文件為text-next.js,寫入以下代碼:
const koa = require("koa"); const app = new koa(); app.use((ctx, next) => { console.log("第一個中間件函數(shù)") next(); console.log("第一個中間件函數(shù)next之后"); }) app.use(async (ctx, next) => { console.log("第二個中間件函數(shù)") next(); console.log("第二個中間件函數(shù)next之后"); }) app.use(ctx => { console.log("響應(yīng)"); ctx.body = "hello" }) app.listen(3000)
以上代碼,可以使用node text-next.js啟動,啟動后可以在瀏覽器中訪問http://localhost:3000/
訪問后,會在啟動的命令窗口中打印出如下值:
第一個中間件函數(shù)
第二個中間件函數(shù)
響應(yīng)
第二個中間件函數(shù)next之后
第一個中間件函數(shù)next之后
是不是對這個順序產(chǎn)生了深深地疑問,為什么會這樣呢?
當(dāng)一個中間件調(diào)用 next() 則該函數(shù)暫停并將控制傳遞給定義的下一個中間件。當(dāng)在下游沒有更多的中間件執(zhí)行后,堆棧將展開并且每個中間件恢復(fù)執(zhí)行其上游行為。
過程是這樣的:
先執(zhí)行第一個中間件函數(shù),打印出 "第一個中間件函數(shù)"
調(diào)用了next,不再繼續(xù)向下執(zhí)行
執(zhí)行第二個中間件函數(shù),打印出 "第二個中間件函數(shù)"
調(diào)用了next,不再繼續(xù)向下執(zhí)行
執(zhí)行最后一個中間件函數(shù),打印出 "響應(yīng)"
...
最后一個中間函數(shù)執(zhí)行后,上一個中間件函數(shù)收回控制權(quán),繼續(xù)執(zhí)行,打印出 "第二個中間件函數(shù)next之后"
第二個中間件函數(shù)執(zhí)行后,上一個中間件函數(shù)收回控制權(quán),繼續(xù)執(zhí)行,打印出 "第一個中間件函數(shù)next之后"
借用一張圖來直觀的說明:
具體看別人怎么理解next的順序:https://segmentfault.com/q/1010000011033764
最近在看Koa的源碼,以上屬于個人理解,如有偏差歡迎指正學(xué)習(xí),謝謝。
參考資料:https://koa.bootcss.com/
https://cnodejs.org/topic/58fd8ec7523b9d0956dad945
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/93024.html
摘要:接上次挖的坑,對相關(guān)的源碼進行分析第一篇。和同為一批人進行開發(fā),與相比,顯得非常的迷你。在接收到一個請求后,會拿之前提到的與來創(chuàng)建本次請求所使用的上下文。以及如果沒有手動指定,會默認(rèn)指定為。 接上次挖的坑,對koa2.x相關(guān)的源碼進行分析 第一篇。 不得不說,koa是一個很輕量、很優(yōu)雅的http框架,尤其是在2.x以后移除了co的引入,使其代碼變得更為清晰。 express和ko...
摘要:從一個對象里面提取需要的屬性這篇文章一直想寫了還想起那一夜我看到白天的代碼,實在太美了。 koa源碼lib主要文件有 application.js context.js request.js response.js application.js koa主要的邏輯處理代碼整個koa的處理 context.js 將req,res方法 掛載在這,生成ctx上下文對象 requests....
摘要:引言最近空閑時間讀了一下的源碼在閱讀的源碼的過程中,我的感受是代碼簡潔思路清晰不得不佩服大神的水平。調(diào)用的時候就跟有區(qū)別使用必須使用來調(diào)用除了上面的的構(gòu)造函數(shù)外,還暴露了一些公用的,比如兩個常見的,一個是,一個是。 引言 最近空閑時間讀了一下Koa2的源碼;在閱讀Koa2(version 2.2.0)的源碼的過程中,我的感受是代碼簡潔、思路清晰(不得不佩服大神的水平)。下面是我讀完之后...
摘要:于是抱著知其然也要知其所以然的想法,開始閱讀的源代碼。問題讀源代碼時,自然是帶著諸多問題的。源代碼如下在被處理完后,每當(dāng)有新請求,便會調(diào)用,去處理請求。接下來會繼續(xù)寫一些閱讀筆記,因為看的源代碼確實是獲益匪淺。 本筆記共四篇Koa源碼閱讀筆記(1) -- coKoa源碼閱讀筆記(2) -- composeKoa源碼閱讀筆記(3) -- 服務(wù)器の啟動與請求處理Koa源碼閱讀筆記(4) -...
摘要:到此為止,我們就基本講清楚了中的中間件洋蔥模型是如何自動執(zhí)行的。 koa被認(rèn)為是第二代web后端開發(fā)框架,相比于前代express而言,其最大的特色無疑就是解決了回調(diào)金字塔的問題,讓異步的寫法更加的簡潔。在使用koa的過程中,其實一直比較好奇koa內(nèi)部的實現(xiàn)機理。最近終于有空,比較深入的研究了一下koa一些原理,在這里會寫一系列文章來記錄一下我的學(xué)習(xí)心得和理解。 在我看來,koa最核心...
閱讀 3807·2021-09-23 11:32
閱讀 2472·2021-09-06 15:01
閱讀 1632·2021-08-18 10:24
閱讀 3470·2019-12-27 11:44
閱讀 3616·2019-08-30 15:52
閱讀 2524·2019-08-30 11:11
閱讀 699·2019-08-29 17:27
閱讀 610·2019-08-29 16:22