大部分人用koa應該是用來實現後端服務的。後端服務最多見的就是實現接口了。但這些接口通常有一些相同的功能。例如日誌打印請求時間,請求參數等。每寫一個接口都寫這些功能,不只浪費時間,代碼也難看。所以將這些通用功能提取成中間件,請求來了以後,通過各個中間件進行處理。後端
首先中間件是由咱們,koa的用戶定義的。因此koa要將咱們定義的中間件存儲起來。咱們應該都寫過中間件,知道每一箇中間件其實就是一個方法。而後中間件的執行是有順序的。因此要存儲一些有順序的方法,最簡單的就是使用一個數組。添加一箇中間件就是爲數組push一個方法。
咱們使用過koa,都知道koa添加中間件的方式是app.use(fn)。app就是koa模塊的實例。數組
class Koa { constructor(){ this.middlewares = [] } use(fn){ this.middlewares.push(fn) } } const app = new Koa() app.use((ctx, next) => {}) console.log(app.middlewares.length) console.log(app.middlewares[0].toString())
稍做思考,咱們就知道讓數組middlewares做爲app的一個屬性,爲app提供一個方法use,功能就是爲middlewares數組push一個方法。上面的代碼會在控制檯上打印出1後打印出(ctx,next)=>{},這裏就不貼圖了。promise
如今咱們已經可以存儲中間件了,那麼如何讓他們按照必定的順序去執行呢?最簡單的就是鏈式執行了。就是一個執行完以後去執行另外一個。代碼也很簡單。閉包
function sleep(time) { return new Promise((resolve)=> { setTimeout(()=>{ resolve() }, time) }) } let middleWare0 = async function (ctx, next) { let startTime = Date.now() if(next){ await next() } console.log(Date.now() - startTime) } let middleWare1 = async function (ctx, next) { if(next){ await next() } await sleep(1000) } async function compose(middlewares, ctx) { for(let middleware of middlewares){ await middleware(ctx) } } compose([middleWare0, middleWare1], {})
compose方法就是用來鏈式執行中間件的。sleep函數就是用來模擬異步操做的。一共有兩個中間件。確實是先執行了中間件0,而後執行了中間件1。這時應該能看出上述代碼有些問題:
首先,咱們在日常寫代碼的時候是沒有if(next)這個判斷的。這裏去掉這個判斷就會報錯。由於在執行中間件的時候根本沒有給他next這個參數。next是undefined,而不是一個函數,因此會報錯。
其次,middleWare0的代碼功能實際上是想等待後面的中間件執行完以後再console.log,但如今確是馬上就輸出了。時間差也是0毫秒左右。而咱們要等待middleWare1完成,應該是有1000ms左右。
**koa要實現的中間件不是簡單的鏈式執行。而是前面的中間件可以控制後面的中間件該什麼時候執行。**很高端吧。其實,不用懼怕。中間件只不過是一個函數罷了,而他想要控制另外一個函數該如何執行,最簡單就是把這個函數告訴他,也就是做爲一個參數傳遞進去。咱們把compose改一改 。app
let compose = async function (ctx) { await middlewares[0](ctx,middlewares[1]) }
首先咱們考慮只有兩個中間件的狀況。上述代碼執行了中間件0,並將ctx和中間件1傳給了它。也就是在中間件0中的next就是中間件1。這和咱們熟悉的不同,next在執行時並不須要咱們傳參數。這時咱們能夠推斷出next這個函數應該是這種形式:koa
next[i] = async function(){ await middlewares[i](ctx,next[i+1]) }
next[i]應該返回的是一個異步函數,裏面執行了當前第i箇中間件,並將ctx,以及next[i+1]傳遞給第i箇中間件。
不難發現,這是一個遞歸的結構。還有對比上面兩段代碼。其實結構是同樣的。因此最終可以寫出這段代碼:異步
function compose(ctx, i) { return async function () { if(middlewares[i]){ await middlewares[i](ctx, compose(ctx, i+1)) } } }
執行compose(ctx,i)便能獲得一個異步函數fn。fn的第一步就是判斷中間件0是否存在,若是存在就調用中間件0,第一個參數爲ctx,第二個參數爲compose(ctx,i+1)這個函數的執行結果,也就是下一個中間件對應的異步函數。下面舉例看下fn。async
let fn = compose(ctx, 0) fn = async function () { await middlewares[0](ctx, async function () { await middlewares[1](ctx, async function () { await middlewares[2](ctx, async function () { ... }) }) }) }
fn的格式就是上面那樣(少了判斷中間件是否存在)。
經過這種方式來執行中間件,每一箇中間件都能控制何時來執行下一個中間件。
不過咱們仍是少了一步,就是如何把middlewares這個數組傳遞進來。個人實現方式不是很好,但看起來比較簡單。函數
let middlewares = [] let setMiddleWare = function (middlewaresArg) { middlewares = middlewaresArg } function compose(ctx, i) { return async function () { if(middlewares[i]){ await middlewares[i](ctx, compose(ctx, i+1)) } } } exports.setMiddleWare = setMiddleWare exports.compose = compose
測試代碼以下:測試
function sleep(time) { return new Promise((resolve)=> { setTimeout(()=>{ resolve() }, time) }) } let middleWare0 = async function (ctx, next) { let startTime = Date.now() await next() console.log(Date.now() - startTime) } let middleWare1 = async function (ctx, next) { next() await sleep(1000) console.log('中間件1') } let middleWare2 = async function (ctx, next) { await sleep(2000) await next() console.log('中間件2') } const compose = require('./compose') compose.setMiddleWare([middleWare0, middleWare1, middleWare2]) let fn = compose.compose({},0) fn() .catch(err => { console.log(err) })
compose即是上面的模塊。若代碼沒有問題,則應該先執行中間件0的await next()前面的代碼,而後執行中間件1,中間件1第一句就是next()也就是異步執行中間件2,這時進入中間件2,中間件2第一句會執行等待定時器2秒。這時回到中間件1,中間1會等待定時器1秒,約一秒後中間件1定時完成,輸出「中間件1」。中間件0等待中間件1執行完成,輸出時間差,約爲1000。約1秒後,中間件2定時完成,輸出「中間件2」。
下面是代碼的輸出結果。
具體的等待時間能夠本身運行驗證下。
到如今爲止咱們基本完成了中間件的全部實現代碼。那麼讓咱們看下koa是如何實現的吧。
//如下代碼均去掉了部分無關代碼 class Application extends Emitter { constructor() { super(); this.middleware = []; } use(fn) { if (typeof fn !== 'function') throw new TypeError('middleware must be a function!'); debug('use %s', fn._name || fn.name || '-'); this.middleware.push(fn); return this; } callback() { const fn = compose(this.middleware); const handleRequest = (req, res) => { const ctx = this.createContext(req, res); return this.handleRequest(ctx, fn); }; return handleRequest; } handleRequest(ctx, fnMiddleware) { return fnMiddleware(ctx).then(handleResponse).catch(onerror); } }
這時看use和callback方法是否是有一些豁然開朗了?
koa的use和咱們寫的相比,主要就是多了一個對入參fn的校驗。 compose中,middleware數組的傳入方式不一樣,他是直接傳進了compose。讓咱們看下koa的compose是如何保存middleware的。
function compose (middleware) { //去掉了校驗代碼 return function (context, next) { let index = -1 return dispatch(0) function dispatch (i) { index = i let fn = middleware[i] if (i === middleware.length) fn = next if (!fn) return Promise.resolve() try { return Promise.resolve(fn(context, function next () { return dispatch(i + 1) })) } catch (err) { return Promise.reject(err) } } } }
koa保存middleware其實用了閉包。compose(middleware)返回了一個function,而這個function用到了middleware。因此只要外面引用這個function,middleware就會一直在這個做用域存在。 compose執行後,返回的function支持兩個參數,一個是context,一個是next。next爲middleware數組後面的中間件。看下代碼中的fn,fn隨着i的增長,而取值爲middleware[i],當i爲middleware的長度時,也就是沒有這個中間件時,fn取值爲傳入的next,因此傳入的next能夠理解爲next[middleware.length] 。 接着看,肯定好了fn後,執行了fn,傳入了參數context,以及next[i+1],和咱們剛剛的代碼是同樣的。不過因爲他用的是promise,因此外面包裹了一層Promise.resolve。方便後面的promise鏈調用。而咱們是用的async,功能都是差很少的。