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)
      }
    }
  }
}

复制代码
相关文章
相关标签/搜索