koa源碼分析(三)中間件

中間件的做用

大部分人用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是如何實現的吧。

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,功能都是差很少的。

相關文章
相關標籤/搜索