中间件执行模块koa-Compose源码分析

原文博客地址,欢迎学习交流:点击预览

读了下Koa的源码,写的相当的精简,遇到处理中间件执行的模块koa-Compose,决定学习一下这个模块的源码。

阅读本文可以学到:

  • Koa中间件的加载
  • next参数的来源
  • 中间件控制权执行顺序

先上一段使用Koa启动服务的代码:
放在文件app.js

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/
访问后,会在启动的命令窗口中打印出如下值:

第一个中间件
第二个中间件
第三个中间件
准备响应

代码说明:

  • app.use()方法,用来将中间件添加到队列中
  • 中间件就是传给app.use()作为的参数的函数
  • 使用app.use()将函数添加至队列之中后,当有请求时,会依次触发队列中的函数,也就是依次执行一个个中间件函数,执行顺序按照调用app.use()添加的顺序。
  • 在每个中间件函数中,会执行next()函数,意思是把控制权交到下一个中间件(实际上是调用next函数后,会调用下一个中间件函数,后面解析源码会有说明),如果不调用next()函数,不能调用下一个中间件函数,那么队列执行也就终止了,在上面的代码中表现就是不能响应客户端的请求了。
app.use(async (ctx,next) => {
    console.log('第二个中间件')
    // next(); 注释之后,下一个中间件函数就不会执行
})

内部过程分析

  • 内部利用app.use()添加到一个数组队列中:
// 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#L104

  • 使用koa-compose模块的compose方法,把这个中间件数组合并成一个大的中间件函数
const fn = compose(this.middleware);

具体参考这里Koa的源码https://github.com/koajs/koa/blob/master/lib/application.js#L126

  • 在有请求后后会执行这个中间件函数fn,进而会把所有的中间件函数依次执行
这样片面的描述可能会不知所云,可以跳过不看,只是让诸位知道Koa执行中间件的过程
本篇主要是分析koa-compose的源码,之后分析整个Koa的源码后会做详细说明

所以最主要的还是使用koa-compose模块来控制中间件的执行,那么来一探究竟这个模块如何进行工作的

koa-compose

koa-compose模块可以将多个中间件函数合并成一个大的中间件函数,然后调用这个中间件函数就可以依次执行添加的中间件函数,执行一系列的任务。

源码地址: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)
      }
    }
  }
}

补充说明:

  • 根据以上的源码分析得到,在一个中间件函数中不能调用两次next(),否则会抛出错误
function one(ctx,next){
    console.log('第一个');
    next();
    next();
}

抛出错误:

next() called multiple times
  • next()调用后返回的是一个Promise对象,可以调用then函数
function two(ctx,next){
    console.log('第二个');
    next().then(function(){
        console.log('第二个调用then后')
    });
}
  • 中间件函数可以是async/await函数,在函数内部可以写任意的异步处理,处理得到结果后再进行下一个中间件函数。

创建一个文件问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,不再继续向下执行
  • 执行第二个中间件函数,打印出 '第二个中间件函数'
  • 调用了next,不再继续向下执行
  • 执行最后一个中间件函数,打印出 '响应'
  • ...
  • 最后一个中间函数执行后,上一个中间件函数收回控制权,继续执行,打印出 '第二个中间件函数next之后'
  • 第二个中间件函数执行后,上一个中间件函数收回控制权,继续执行,打印出 '第一个中间件函数next之后'

借用一张图来直观的说明:
图片描述

具体看别人怎么理解next的顺序:https://segmentfault.com/q/1010000011033764

最近在看Koa的源码,以上属于个人理解,如有偏差欢迎指正学习,谢谢。

参考资料:https://koa.bootcss.com/
https://cnodejs.org/topic/58fd8ec7523b9d0956dad945

阅读 4.6k

推荐阅读
前端集结号
用户专栏

分享自己学习前端的点点滴滴,希望和志同道合的人一起交流学习。

72 人关注
27 篇文章
专栏主页