原文博客地址,歡迎學習交流: 點擊預覽
讀了下Koa的源碼,寫的至關的精簡,遇處處理中間件執行的模塊koa-Compose,決定學習一下這個模塊的源碼。css
閱讀本文能夠學到:node
先上一段使用Koa啓動服務的代碼:
放在文件app.js中git
const koa = require('koa'); // require引入koa模塊 const app = new koa(); // 建立對象 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('準備響應'); ctx.body = 'hello' }) app.listen(3000)
以上代碼,可使用node app.js啓動,啓動後能夠在瀏覽器中訪問http://localhost:3000/
訪問後,會在啓動的命令窗口中打印出以下值:github
第一個中間件
第二個中間件
第三個中間件
準備響應
代碼說明:數據庫
app.use(async (ctx,next) => { console.log('第二個中間件') // next(); 註釋以後,下一個中間件函數就不會執行 })
// app.use()函數內部添加 this.middleware.push(fn); // 最終this.middleware爲: this.middleware = [fn,fn,fn...]
具體參考這裏Koa的源碼use函數:https://github.com/koajs/koa/blob/master/lib/application.js#L104segmentfault
const fn = compose(this.middleware);
具體參考這裏Koa的源碼https://github.com/koajs/koa/blob/master/lib/application.js#L126api
這樣片面的描述可能會不知所云,能夠跳過不看,只是讓諸位知道Koa執行中間件的過程
本篇主要是分析 koa-compose的源碼,以後分析整個Koa的源碼後會作詳細說明
因此最主要的仍是使用koa-compose模塊來控制中間件的執行,那麼來一探究竟這個模塊如何進行工做的數組
koa-compose模塊能夠將多箇中間件函數合併成一個大的中間件函數,而後調用這個中間件函數就能夠依次執行添加的中間件函數,執行一系列的任務。promise
源碼地址:https://github.com/koajs/compose/blob/master/index.js瀏覽器
先從一段代碼開始,建立一個compose.js的文件,寫入以下代碼:
const compose = require('koa-compose'); function one(ctx,next){ console.log('第一個'); next(); // 控制權交到下一個中間件(其實是能夠執行下一個函數), } function two(ctx,next){ console.log('第二個'); next(); } function three(ctx,next){ console.log('第三個'); next(); } // 傳入中間件函數組成的數組隊列,合併成一箇中間件函數 const middlewares = compose([one, two, three]); // 執行中間件函數,函數執行後返回的是Promise對象 middlewares().then(function (){ console.log('隊列執行完畢'); })
可使用node compose.js運行此文件,命令行窗口打印出:
第一個
第二個
第三個
隊列執行完畢
中間件這兒的重點,是compose函數。compose函數的源代碼雖然很簡潔,但要理解明白着實要下一番功夫。
如下爲源碼分析:
'use strict' /** * Expose compositor. */ // 暴露compose函數 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函數須要傳入一個數組隊列 [fn,fn,fn,fn] 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!') } /** * @param {Object} context * @return {Promise} * @api public */ // compose函數調用後,返回的是如下這個匿名函數 // 匿名函數接收兩個參數,第一個隨便傳入,根據使用場景決定 // 第一次調用時候第二個參數next其實是一個undefined,由於初次調用並不須要傳入next參數 // 這個匿名函數返回一個promise return function (context, next) { // last called middleware # //初始下標爲-1 let index = -1 return dispatch(0) function dispatch (i) { // 若是傳入i爲負數且<=-1 返回一個Promise.reject攜帶着錯誤信息 // 因此執行兩次next會報出這個錯誤。將狀態rejected,就是確保在一箇中間件中next只調用一次 if (i <= index) return Promise.reject(new Error('next() called multiple times')) // 執行一遍next以後,這個index值將改變 index = i // 根據下標取出一箇中間件函數 let fn = middleware[i] // next在這個內部中是一個局部變量,值爲undefined // 當i已是數組的length了,說明中間件函數都執行結束,執行結束後把fn設置爲undefined // 問題:原本middleware[i]若是i爲length的話取到的值已是undefined了,爲何要從新給fn設置爲undefined呢? if (i === middleware.length) fn = next //若是中間件遍歷到最後了。那麼。此時return Promise.resolve()返回一個成功狀態的promise // 方面以後作調用then if (!fn) return Promise.resolve() // try catch保證錯誤在Promise的狀況下可以正常被捕獲。 // 調用後依然返回一個成功的狀態的Promise對象 // 用Promise包裹中間件,方便await調用 // 調用中間件函數,傳入context(根據場景不一樣能夠傳入不一樣的值,在KOa傳入的是ctx) // 第二個參數是一個next函數,可在中間件函數中調用這個函數 // 調用next函數後,遞歸調用dispatch函數,目的是執行下一個中間件函數 // next函數在中間件函數調用後返回的是一個promise對象 // 讀到這裏不得不佩服做者的高明之處。 try { return Promise.resolve(fn(context, function next () { return dispatch(i + 1) })) } catch (err) { return Promise.reject(err) } } } }
補充說明:
function one(ctx,next){ console.log('第一個'); next(); next(); }
拋出錯誤:
next() called multiple times
function two(ctx,next){ console.log('第二個'); next().then(function(){ console.log('第二個調用then後') }); }
建立一個文件問test-async.js,寫入如下代碼:
const compose = require('koa-compose'); // 獲取數據 const getData = () => new Promise((resolve, reject) => { setTimeout(() => resolve('獲得數據'), 2000); }); async function one(ctx,next){ console.log('第一個,等待兩秒後再進行下一個中間件'); // 模擬異步讀取數據庫數據 await getData() // 等到獲取數據後繼續執行下一個中間件 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('隊列執行完畢'); })
可使用node test-async.js運行此文件,命令行窗口打印出:
第一個,等待兩秒後再進行下一個中間件
第二個
第三個
第二個調用then後
隊列執行完畢
在以上打印輸出過程當中,執行第一個中間件後,在內部會有一個異步操做,使用了async/await後獲得同步操做同樣的體驗,這步操做多是讀取數據庫數據或者讀取文件,讀取數據後,調用next()執行下一個中間件。這裏模擬式等待2秒後再執行下一個中間件。
更多參考了async/await: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/async_function
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/await
調用next後,執行的順序會讓人產生迷惑,建立文件爲text-next.js,寫入如下代碼:
const koa = require('koa'); const app = new koa(); app.use((ctx, next) => { console.log('第一個中間件函數') next(); console.log('第一個中間件函數next以後'); }) app.use(async (ctx, next) => { console.log('第二個中間件函數') next(); console.log('第二個中間件函數next以後'); }) app.use(ctx => { console.log('響應'); ctx.body = 'hello' }) app.listen(3000)
以上代碼,可使用node text-next.js啓動,啓動後能夠在瀏覽器中訪問http://localhost:3000/
訪問後,會在啓動的命令窗口中打印出以下值:
第一個中間件函數
第二個中間件函數
響應
第二個中間件函數next以後
第一個中間件函數next以後
是否是對這個順序產生了深深地疑問,爲何會這樣呢?
當一箇中間件調用 next() 則該函數暫停並將控制傳遞給定義的下一個中間件。當在下游沒有更多的中間件執行後,堆棧將展開而且每一箇中間件恢復執行其上游行爲。
過程是這樣的:
借用一張圖來直觀的說明:
具體看別人怎麼理解next的順序:https://segmentfault.com/q/1010000011033764
最近在看Koa的源碼,以上屬於我的理解,若有誤差歡迎指正學習,謝謝。
參考資料:https://koa.bootcss.com/
https://cnodejs.org/topic/58fd8ec7523b9d0956dad945