资讯专栏INFORMATION COLUMN

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

imtianx / 2346人阅读

摘要:原文博客地址,欢迎学习交流点击预览读了下的源码,写的相当的精简,遇到处理中间件执行的模块决定学习一下这个模块的源码。当在下游没有更多的中间件执行后,堆栈将展开并且每个中间件恢复执行其上游行为。

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

读了下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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/93024.html

相关文章

  • koa源码阅读[1]-koa与koa-compose

    摘要:接上次挖的坑,对相关的源码进行分析第一篇。和同为一批人进行开发,与相比,显得非常的迷你。在接收到一个请求后,会拿之前提到的与来创建本次请求所使用的上下文。以及如果没有手动指定,会默认指定为。 接上次挖的坑,对koa2.x相关的源码进行分析 第一篇。 不得不说,koa是一个很轻量、很优雅的http框架,尤其是在2.x以后移除了co的引入,使其代码变得更为清晰。 express和ko...

    vibiu 评论0 收藏0
  • koa源码阅读之目录结构与辅助库相关

    摘要:从一个对象里面提取需要的属性这篇文章一直想写了还想起那一夜我看到白天的代码,实在太美了。 koa源码lib主要文件有 application.js context.js request.js response.js application.js koa主要的逻辑处理代码整个koa的处理 context.js 将req,res方法 挂载在这,生成ctx上下文对象 requests....

    sherlock221 评论0 收藏0
  • Koa2源码阅读笔记

    摘要:引言最近空闲时间读了一下的源码在阅读的源码的过程中,我的感受是代码简洁思路清晰不得不佩服大神的水平。调用的时候就跟有区别使用必须使用来调用除了上面的的构造函数外,还暴露了一些公用的,比如两个常见的,一个是,一个是。 引言 最近空闲时间读了一下Koa2的源码;在阅读Koa2(version 2.2.0)的源码的过程中,我的感受是代码简洁、思路清晰(不得不佩服大神的水平)。下面是我读完之后...

    plus2047 评论0 收藏0
  • Koa源码阅读笔记(2) -- compose

    摘要:于是抱着知其然也要知其所以然的想法,开始阅读的源代码。问题读源代码时,自然是带着诸多问题的。源代码如下在被处理完后,每当有新请求,便会调用,去处理请求。接下来会继续写一些阅读笔记,因为看的源代码确实是获益匪浅。 本笔记共四篇Koa源码阅读笔记(1) -- coKoa源码阅读笔记(2) -- composeKoa源码阅读笔记(3) -- 服务器の启动与请求处理Koa源码阅读笔记(4) -...

    roland_reed 评论0 收藏0
  • 深入探析koa之间件流程控制篇

    摘要:到此为止,我们就基本讲清楚了中的中间件洋葱模型是如何自动执行的。 koa被认为是第二代web后端开发框架,相比于前代express而言,其最大的特色无疑就是解决了回调金字塔的问题,让异步的写法更加的简洁。在使用koa的过程中,其实一直比较好奇koa内部的实现机理。最近终于有空,比较深入的研究了一下koa一些原理,在这里会写一系列文章来记录一下我的学习心得和理解。 在我看来,koa最核心...

    fuchenxuan 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<