koa2中间件koa和koa-compose源码分析原理(一)

koa是基于nodejs平台的下一代web开发框架,它是使用generator和promise,koa的中间件是一系列generator函数的对象。
当对象被请求过来的时候,会依次经过各个中间件进行处理,当有yield next就跳到下一个中间件,当中间件没有 yield next执行的时候,然后就会逆序执行前面那些中间件剩下的逻辑代码,比如看如下的demo:

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

app.use(async (ctx, next) => {
  console.log(11111);
  await next();
  console.log(22222);
});

app.use(async (ctx, next) => {
  console.log(33333);
  await next();
  console.log(44444);
});

app.use(async (ctx, next) => {
  console.log(55555);
  await next();
  console.log(66666);
});
app.listen(3001);
console.log(‘app started at port 3000...‘);

// 执行结果为 11111  33333 55555 66666 44444 22222

当我们在浏览器访问 http://localhost:3001 的时候,会分别输出 11111 33333 55555 66666 44444 22222,如上代码是如下执行的:请求的时候,会执行第一个use里面的异步函数代码,先打印出 11111,然后碰到 await next() 函数,就执行第二个中间件,就会打印 33333, 然后又碰到 await next()后,就会跳转到下一个中间件,因此会打印 55555, 然后再碰到 awaitnext() 方法后,由于下面没有中间件了,因此先会打印 666666, 然后依次逆序返回上面未执行完的代码逻辑,然后我们就会打印44444,再依次就会打印 22222 了。

它的结构网上都叫洋葱结构,当初为什么要这样设计呢?因为是为了解决复杂应用中频繁的回调而设计的级联代码,它并不会把控制权完全交给一个中间件的代码,而是碰到next就会去下一个中间件,等下面所有中间件执行完成后,就会再回来执行中间件未完成的代码,我们上面说过koa是由一系列generator函数对象的,如果我们不使用koa的async语法的话,我们可以再来看下使用generator函数来实现如下:

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

app.use(function *(next) {
  // 第一步进入
  const start = new Date;
  console.log(‘我是第一步‘);
  yield next;

  // 这是第五步进入的
  const ms = new Date - start;
  console.log(ms + ‘ms‘);
});

app.use(function *(next) {
  // 这是第二步进入的
  const start = new Date;
  console.log(‘我是第二步‘);
  yield next;
  // 这是第四步进入的
  const ms = new Date - start;
  console.log(‘我是第四步‘ + ms);
  console.log(this.url);
});

// response
app.use(function *() {
  console.log(‘我是第三步‘);
  this.body = ‘hello world‘;
});
app.listen(3001);
console.log(‘app started at port 3000...‘);

执行的结果如下所示:

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

koa部分源码如下:

module.exports = class Application extends Emitter {
  /**
   * Initialize a new `Application`.
   *
   * @api public
   */

  constructor() {
    super();

    this.proxy = false;
    this.middleware = [];
    this.subdomainOffset = 2;
    this.env = process.env.NODE_ENV || ‘development‘;
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);
    if (util.inspect.custom) {
      this[util.inspect.custom] = this.inspect;
    }
  }
  /**
   * Use the given middleware `fn`.
   *
   * Old-style middleware will be converted.
   *
   * @param {Function} fn
   * @return {Application} self
   * @api public
   */
  use(fn) {
    if (typeof fn !== ‘function‘) throw new TypeError(‘middleware must be a function!‘);
    if (isGeneratorFunction(fn)) {
      deprecate(‘Support for generators will be removed in v3. ‘ +
                ‘See the documentation for examples of how to convert old middleware ‘ +
                ‘https://github.com/koajs/koa/blob/master/docs/migration.md‘);
      fn = convert(fn);
    }
    debug(‘use %s‘, fn._name || fn.name || ‘-‘);
    this.middleware.push(fn);
    return this;
  }

  /**
   * Return a request handler callback
   * for node‘s native http server.
   *
   * @return {Function}
   * @api public
   */
  callback() {
    const fn = compose(this.middleware);

    if (!this.listenerCount(‘error‘)) this.on(‘error‘, this.onerror);

    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn);
    };

    return handleRequest;
  }

  /**
   * Handle request in callback.
   *
   * @api private
   */

  handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    res.statusCode = 404;
    const onerror = err => ctx.onerror(err);
    const handleResponse = () => respond(ctx);
    onFinished(res, onerror);
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }

  /**
   * Initialize a new context.
   *
   * @api private
   */

  createContext(req, res) {
    const context = Object.create(this.context);
    const request = context.request = Object.create(this.request);
    const response = context.response = Object.create(this.response);
    context.app = request.app = response.app = this;
    context.req = request.req = response.req = req;
    context.res = request.res = response.res = res;
    request.ctx = response.ctx = context;
    request.response = response;
    response.request = request;
    context.originalUrl = request.originalUrl = req.url;
    context.state = {};
    return context;
  }

  listen(...args) {
    debug(‘listen‘);
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }
}

如上就是koa部分主要代码,和 koa-compose 源码;首先先看 koa的源码中 use 方法,use方法的作用是把所有的方法存入到一个全局数组middleware里面去,然后返回 this,目的使函数能链式调用。我们之前做的demo如下这样的:

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

app.use(function *(next) {
  // 第一步进入
  const start = new Date;
  console.log(‘我是第一步‘);
  yield next;

  // 这是第五步进入的
  const ms = new Date - start;
  console.log(ms + ‘ms‘);
});

app.use(function *(next) {
  // 这是第二步进入的
  const start = new Date;
  console.log(‘我是第二步‘);
  yield next;
  // 这是第四步进入的
  const ms = new Date - start;
  console.log(‘我是第四步‘ + ms);
  console.log(this.url);
});

// response
app.use(function *() {
  console.log(‘我是第三步‘);
  this.body = ‘hello world‘;
});
app.listen(3001);
console.log(‘app started at port 3000...‘);

我们来理解下,我们会把 app.use(function *(){}) 这样的函数会调用use方法,然后use函数内部会进行判断是不是函数,如果不是函数会报错,如果是函数的话,就转换成 async 这样的函数,然后才会依次存入 middleware这个全局数组里面去,存入以后,我们需要怎么调用呢?我们下面会 使用 app.listen(3001), 这样启动一个服务器,然后我们就会调用 koa中的listen这个方法,端口号是3001,listen方法上面有代码,我们复制下来一步步来理解下;如下基本代码:

listen(...args) {
  debug(‘listen‘);
  const server = http.createServer(this.callback());
  return server.listen(...args);
}

如上代码,它是通过node基本语法创建一个服务器,const server = http.createServer(this.callback()); 这句代码就会执行 callback这个方法,来调用,可能看这个方法,我们不好理解,这个方法和下面的基本方法是类似的;如下node基本代码:

var http = require("http")
http.createServer(function(req,res){
  res.writeHead(200,{‘Content-Type‘:‘text/html‘});
  res.write("holloe  world")   
  res.end("fdsa");
}).listen(8000);

如上代码我是创建一个8000服务器,当我们访问 http://localhost:8000/ 的时候,我们会调用 http.createServer 中的function函数代码,然后会打印数据,因此该方法是自动执行的。因此上面的listen方法也是这个道理的,会自动调用callback()方法内部代码执行的,因此koa中的callback代码如下:

/**
 * Return a request handler callback
 * for node‘s native http server.
 *
 * @return {Function}
 * @api public
 */
callback() {
  const fn = compose(this.middleware);

  if (!this.listenerCount(‘error‘)) this.on(‘error‘, this.onerror);

  const handleRequest = (req, res) => {
    const ctx = this.createContext(req, res);
    return this.handleRequest(ctx, fn);
  };

  return handleRequest;
}

然后会调用 const fn = compose(this.middleware); 这句代码,该compose 代码会返回一个函数,compose函数代码(也就是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)
      }
    }
  }
}

compose 函数代码 传入一个数组的中间件 middleware, 首先判断是不是数组,然后判断是不是函数,该参数 middleware 就是我们把use里面的所有函数存入该数组中的。该函数会返回一个函数。

然后继续往下执行 callback中的代码,如下代码执行:

const handleRequest = (req, res) => {
  const ctx = this.createContext(req, res);
  return this.handleRequest(ctx, fn);
};

return handleRequest;

首先会创建一个上下文对象 ctx,具体怎么创建可以看 koa源码中的 createContext 这个方法,然后会调用 koa中的handleRequest(ctx, fn)这个方法, 该方法传递二个参数,第一个是ctx,指上下文对象,第二个是 compose 函数中返回的函数,koa中的 handleRequest函数代码如下:

handleRequest(ctx, fnMiddleware) {
  const res = ctx.res;
  res.statusCode = 404;
  const onerror = err => ctx.onerror(err);
  const handleResponse = () => respond(ctx);
  onFinished(res, onerror);
  return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}

最后一句代码 fnMiddleware(ctx).then(handleResponse).catch(onerror); 中的 fnMiddleware(ctx) 就会调用koa-compose 中返回的函数的代码,compose 函数返回的代码如下函数:

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

依次执行 app.use(function(req, res) {}) 中内这样的函数,会执行如上 dispatch方法,从0开始,也就是说从第一个函数开始执行,然后就会执行完成后,会返回一个promise对象,Promise.resolve(fn(context, dispatch.bind(null, i + 1))); dispatch.bind(null, i + 1)) 该函数的作用是循环调用dispatch方法,返回promise对象后,执行then方法就会把值返回回来,因此执行所有的 app.use(function(req, res) {}); 里面这样的function方法,dispatch(i + 1) 就是将数组指针移向下一个,执行下一个中间件的代码,然后一直这样到最后一个中间件,这就是一直use,然后next方法执行到最后的基本原理,但是我们从上面知道,我们执行完所有的use方法后,并没有像洋葱的结构那样?那怎么回去的呢?其实回去的代码其实就是函数压栈和出栈,比如我们可以看如下代码就可以理解其函数的压栈和出栈的基本原理了。

如下函数代码:

function test1() {
  console.log(1)
  test2();
  console.log(5)
  return Promise.resolve();
}
function test2() {
  console.log(2)
  test3();
  console.log(4)
}

function test3() {
  console.log(3)
  return;
}
test1();

打印的顺序分别为 1, 2, 3, 4, 5;

如上代码就是koa的执行分析的基本原理了。

原文地址:https://www.cnblogs.com/tugenhua0707/p/10204009.html

时间: 2024-10-08 08:56:39

koa2中间件koa和koa-compose源码分析原理(一)的相关文章

爬虫5 scrapy框架2 全站爬取cnblogs, scarpy请求传参, 提高爬取效率, 下载中间件, 集成selenium, fake-useragent, 去重源码分析, 布隆过滤器, 分布式爬虫, java等语言概念补充, bilibili爬视频参考

1 全站爬取cnblogs # 1 scrapy startproject cnblogs_crawl # 2 scrapy genspider cnblogs www.cnblogs.com 示例: # cnblogs_crawl/cnblogs_crawl/spiders/cnblogs.py import scrapy from cnblogs_crawl.items import CnblogsCrawlItem from scrapy.http import Request class

Android异步消息处理机制详解及源码分析

PS一句:最终还是选择CSDN来整理发表这几年的知识点,该文章平行迁移到CSDN.因为CSDN也支持MarkDown语法了,牛逼啊! [工匠若水 http://blog.csdn.net/yanbober 转载烦请注明出处,尊重分享成果] 最近相对来说比较闲,加上养病,所以没事干就撸些自己之前的知识点为博客,方便自己也方便别人. 1 背景 之所以选择这个知识点来分析有以下几个原因: 逛GitHub时发现关注的isuss中有人不停的在讨论Android中的Looper , Handler , Me

Redux源码分析之compose

Redux源码分析之基本概念 Redux源码分析之createStore Redux源码分析之bindActionCreators Redux源码分析之combineReducers Redux源码分析之compose      解读之前先了准备一下基本知识 rest参数  形式为...变量名,用于获取函数的多余参数 ,该变量将多余的参数放入数组中, 只能是参数的最后一个. 扩展运算符 扩展运算符(spread)是三个点(...).它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序

keystone源码分析(一)——Paste Deploy的应用

本keystone源码分析系列基于Juno版Keystone,于2014年10月16日随Juno版OpenStack发布. Keystone作为OpenStack中的身份管理与授权模块,主要实现系统用户的身份认证.基于角色的授权管理.其他OpenStack服务的地址发现和安全策略管理等功能.Keystone作为开源云系统OpenStack中至关重要的组成部分,与OpenStack中几乎所有的其他服务(如Nova, Glance, Neutron等)都有着密切的联系.同时,Keystone作为开源

Tomcat7.0源码分析——请求原理分析(上)

前言 谈起Tomcat的诞生,最早可以追溯到1995年.近20年来,Tomcat始终是使用最广泛的Web服务器,由于其使用Java语言开发,所以广为Java程序员所熟悉.很多人早期的J2EE项目,由程序员自己实现Jsp页面或者Servlet接受请求,后来借助Struts1.Struts2.Spring等中间件后,实际也是利用Filter或者Servlet处理请求,大家肯定要问了,这些Servlet处理的请求来自哪里?Tomcat作为Web服务器是怎样将HTTP请求交给Servlet的呢? 本文就

Beego源码分析(转)

摘要 beego 是 @astaxie 开发的重量级Go语言Web框架.它有标准的MVC模式,完善的功能模块,和优异的调试和开发模式等特点.并且beego在国内企业用户较多,社区发达和Q群,文档齐全,特别是 @astaxie 本人对bug和issue等回复和代码修复很快,非常敬业.beego框架本身模块众多,无法简单描述所有的功能.我简单阅读了源码,记录一下beego执行过程.官方文档已经图示了beego执行过程图,而我会比较详细的解释beego的源码实现. beego 是 @astaxie 开

cJSON源码分析

cJSON源码分析 简介 由于C语言汇总,没有直接的字典,字符串数组等数据结构,所以要借助结构体定义,处理json. JSON是一种轻量级的数据交换格式.JSON采用完全独立与语言的文本格式,易于人阅读和编写.同时也易于机器解析和生成.它是基于JavaScript,Programming Language,Standard ECMA-262 3rd Edition -December 1999的一个子集.JSON采用完全独立于语言的文本格式,但是也使用了类似于C语言家族的习惯(如C,C++,C+

Tomcat7.0源码分析——请求原理分析

Tomcat7.0源码分析--请求原理分析 谈起Tomcat的诞生,最早可以追溯到1995年.近20年来,Tomcat始终是使用最广泛的Web服务器,由于其使用Java语言开发,所以广为Java程序员所熟悉.很多人早期的J2EE项目,由程序员自己实现Jsp页面或者Servlet接受请求,后来借助Struts1.Struts2.spring等中间件后,实际也是利用Filter或者Servlet处理请求,大家肯定要问了,这些Servlet处理的请求来自哪里?Tomcat作为Web服务器是怎样将HTT

Nginx源码分析 - Nginx启动以及IOCP模型

Nginx 源码分析 - Nginx启动以及IOCP模型 版本及平台信息 本文档针对Nginx1.11.7版本,分析Windows下的相关代码,虽然服务器可能用linux更多,但是windows平台下的代码也基本相似 ,另外windows的IOCP完成端口,异步IO模型非常优秀,很值得一看. Nginx启动 曾经有朋友问我,面对一个大项目的源代码,应该从何读起呢?我给他举了一个例子,我们学校大一大二是在紫金港校区,到了 大三搬到玉泉校区,但是大一的时候也会有时候有事情要去玉泉办.偶尔会去玉泉,但