nodejs中koa2中間件原理分析

一.Koa2中間件的使用方式

官網代碼示例css

1.新建一個項目,命名爲koa2-testnode

2.在命令行中,進入koa2-test,執行npm init -ynpm

npm init -y
複製代碼

3.將此項目中的package.json中的"main":"index.js"替換爲"main":"app.js" json

npm install koa --save
複製代碼

4.建立app.js文件,將以下代碼複製到app.js中,(一共有三個中間件),如下代碼爲官網示例代碼api

const Koa = require('koa');
const app = new Koa();

// logger 記錄日誌

app.use(async (ctx, next) => {
  await next();
  const rt = ctx.response.get('X-Response-Time');
  console.log(`${ctx.method} ${ctx.url} - ${rt}`);
});

// x-response-time 處理請求時間

app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  ctx.set('X-Response-Time', `${ms}ms`);
});

// response

app.use(async ctx => {
  ctx.body = 'Hello World';
});

app.listen(3000);
複製代碼

5.在命令行中啓動(啓動命令以下)數組

node app.js
複製代碼

6.瀏覽器訪問 localhost:3000promise

命令行中打印以下:瀏覽器

代碼執行流程解析:bash

1.先註冊3箇中間件,再監聽3000端口。app

2.在第一個logger中間件執行到await next();時,下面的代碼先不執行,繼續執行下一個中間件x-response-time,直到遇到ctx.body,再開始逆向執行await next();下的內容,最終打印出響應所需的時間。 此流程即爲洋蔥圈模型:(此圖爲網上搜索獲得)

將代碼進行以下修改,加入註釋

const Koa = require('koa');
const app = new Koa();

// logger 記錄日誌

app.use(async (ctx, next) => {
  console.log("第一層洋蔥---開始")
  await next();
  const rt = ctx.response.get('X-Response-Time');
  console.log(`${ctx.method} ${ctx.url} - ${rt}`);
  console.log("第一層洋蔥---結束")
});

// x-response-time 處理請求時間

app.use(async (ctx, next) => {
  console.log("第二層洋蔥---開始")
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  ctx.set('X-Response-Time', `${ms}ms`);
  console.log("第二層洋蔥---結束")
});

// response

app.use(async ctx => {
  console.log("第三層洋蔥---開始")
  ctx.body = 'Hello World';
  console.log("第三層洋蔥---結束")
});

app.listen(3000);

複製代碼

請求在命令行打印以下:這樣就清楚的解釋了洋蔥圈模型(由外向內執行,再由內向外執行)

此行爲用洋蔥來形容很是的形象,先由外向內執行一個一個的next,等執行到最中心,再由內向外一層層執行。

二.分析如何實現,並用代碼模擬實現

從上面的例子中咱們能夠進行分析,中間件是如何進行實現的? 猜想至少應該有兩個步驟:

1.app.use用來註冊中間件,並進行收集

2.實現next機制:經過上一個next觸發下一個next

// 引入http
const http = require('http')

// 組合中間件
function compose(middlewareList) {
    return function (ctx) {
    // 中間件調用的邏輯
      function dispatch(i) {
          const fn = middlewareList[i]
          try {
              return Promise.resolve(
                // 執行中間件,並封裝爲Promise,格式兼容
                fn(ctx, dispatch.bind(null, i + 1)) // Promise
              )
          } catch (err) {
              return Promise.reject(err)
          }
      }
      return dispatch(0)
    }
}

// 定義構造函數
class Koa2 {
    constructor () {
        // 中間件數組
        this.middlewareList = []
    }
    
    use(fn) {
        this.middlewareList.push(fn)
        return this
    }
    
    // 將req和res組合爲ctx
    createContext(req, res) {
        const ctx = {
            req,
            res
        }
        ctx.query = req.query
        return ctx
    }
    
    handleRequest(ctx, fn) {
        return fn(ctx)
    }
    
    callback() {
        const fn = compose(this.middlewareList)
    
        return (req, res) => {
            const ctx = this.createContext(req, res)
            return this.handleRequest(ctx, fn)
        }
    }
    // 建立服務並監聽  ...args傳入多個參數
    listen(...args) {
        const server = http.createServer(this.callback())
        server.listen(..args)
    }
}

module.exports = Koa2
複製代碼

結構爲:在class Koa2中有 use createContext callback listen等方法!經過use方法來收集中間件。 compose爲組合中間件的方法,從而實現next(),其中Promise.resolve()是爲了防止,在使用app.use()時沒有使用async包裹,就返回的不是promise函數,Promise.resolve()包裹後就一直返回promise

fn(ctx, dispatch.bind(null, i + 1))包裹在Promise.resolve()中,fnasync函數

  • Promise.resolve(value)方法返回一個以給定值解析後的Promise對象。若是該值(指代value)爲promise,返回這個promise;若是value值爲promise,返回這個promise此函數將類promise對象的多層嵌套展平。

  • MDN的解釋中,若是Promise.resolve(value)中的value值爲promise,則返回這個promise。在KOA2中,中間件爲async await包裹的異步函數,而async awaitpromise的語法糖。所以即便用Promise.resolve(value)把中間件進行了包裹,也會不想影響結果,並且避免了中間件沒有使用async await時的報錯。

這篇文章中也有說起。

三.代碼分析

compose爲組合中間件的方法,其實也就不難看出,整個中間件的核心功能就在compose,此方法將中間件pushmiddlewareList中。

所以重點在於在compose中進行遞歸,在監聽到request請求的時候,將上下文對象ctx傳入其中,最終使全部中間件按照洋蔥圈模型執行。

middlewares數組合成到最後一箇中間件的時候,則直接返回,此時遞歸則結束。

Promise.resolve()
複製代碼

遞歸的返回值爲何要通過Promise.resolve()的包裹呢?由於涉及到async、await等相關的異步操做。若在使用app.use()時未用async包裹則會發生錯誤。

咱們最終的目的是返回一個可接收上下文參數ctx的函數,所以須要對dispatch進行進一步的包裝,就造成了咱們最終的compose,dispatch執行的過程是一個遞歸的過程。

function compose(middlewareList) {
    reutrn function(ctx) {
       function dispatch(i) {
           const fn = middlewareList[i]
           try {
               return Promise.resolve(
                 fn(ctx, dispatch.bind(null, i+1))
               )
           } catch (err) {
               return Promise.reject(err)
           }
       }
       reutrn dispatch(0)
    }
}
複製代碼

KOA源碼結構以下圖:

lib文件夾下放着四個 KOA2核心文件, application.js、context.js、request.js、response.js

koa-compose源碼以下

'use strict'

/**
 * Expose compositor.
 */

module.exports = compose

/**
 * Compose `middleware` returning
 * a fully valid middleware comprised
 * of all those which are passed.
 *
 * @param {Array} middleware
 * @return {Function}
 * @api public
 */

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
   */

  return function (context, next) {
    // last called middleware #
    let index = -1
    return dispatch(0)
    function dispatch (i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      let fn = middleware[i]
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

複製代碼
相關文章
相關標籤/搜索