koa2中间件实现源码解析

众所周知,koa2核心的部分就是middleware和context了,本文将从结合官网demo以及源码对其进行解读

官网例子使用

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

// 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`);
});

// logger

app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  console.log(`${ctx.method} ${ctx.url} - ${ms}`);
});

// response

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

app.listen(3000);
复制代码

上述代码做的事

  • 1.require('koa')执行koa源码中lib/application.js的导出部分(这是因为可以看到koa源码中的package.json文件的main为application.js,默认为Index.js),new Koa对其导出进行实例化,便于我们调用application.js上的相关方法
  • 2.既然已经拿到方法了,app.use就是调用application的use方法,这里就是初始化中间件,这里每一个use就是一个中间件,接下来解读源码会提到
  • 3.app.listen(3000),这里除了大家可以想到的nodejs的http server模块监听3000端口,并且传入req,res对象还有更重要的事,接收到请求使用中间件,同样解读源码会详细解释

源码解读-中间件原理

根据上述代码做的事,我们从2开始:

首先http.createrServer,ceateServer方法接受一个函数作为参数,这个函数的两个参数分别为request和response; 而在koa中接受一个callback,就是下面代码中的this.callback

listen(...args) {
    debug('listen');
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }
复制代码

上述callback已经执行,所以真正的createrServer接受的函数应该是callback的返回值,所以这和ceateServer方法接受一个函数作为参数不矛盾,本质上都是一样的,接下来看具体的callback函数

callback() {
    const fn = compose(this.middleware);

    if (!this.listeners('error').length) this.on('error', this.onerror);

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

    return handleRequest;
  }
复制代码

返回值是const定义的handleRequest函数,这个函数首先创建一个context(在context挂载http请求响应状态等一些信息),然后将创建好的ctx和一个fn作为参数传给了this.handleReguest;ctx很好理解,就是将一些http模块的req,res挂载到ctx上去,也就是我们koa的重要组成部分之一,请求上下文

  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.cookies = new Cookies(req, res, {
      keys: this.keys,
      secure: request.secure
    });
    request.ip = request.ips[0] || req.socket.remoteAddress || '';
    context.accept = request.accept = accepts(req);
    context.state = {};
    return context;
  }
复制代码

关键是compose(this.middleware),他的参数this.middleware是个数组,会在使用koa的use方法时,push进去函数,也就是我们开头所提到的初始化中间件

  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); //注释提到了,generators将被弃用,这里做了转换,可以看koa-convert源码,使用co模块进行转换,转换成promise对象,并且自动调用Generator next对象,co模块实现之类的可以自行去看源码,这里不多赘述
    }
    debug('use %s', fn._name || fn.name || '-');
    this.middleware.push(fn);  //这里是关键,在middleware数组里面添加了各个中间件方法,也就是app.use传入的函数
    return this;
  }
复制代码

再来看compose源码

function compose (middleware) {
  //一些类型判断,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 //避免同一个中间件多次调用next
      let fn = middleware[i] //获得中间件函数
      if (i === middleware.length) fn = next  //递归出口,调用结束
      if (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, function next () {
          return dispatch(i + 1)
        }))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

复制代码

可以通过源码看出,传递给this.handleRequest(ctx, fn)的fn就是这个compose的返回值,一个匿名函数: 接下里我们先不看这个匿名函数,继续看到this.handleRequest,既然已经明确了它的两个参数ctx和fn:

 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);  //匿名函数中的next应该始终是undefined,用来控制结束
  }
复制代码

之前说到fn就是compose返回的匿名函数,现在又将fn传递给了handleRequest,

控制权转到fnMiddleware中,所以fnMiddlewar就是刚才我们没有看的匿名函数,这里通过尾递归调用依次控制执行中间件函数,

最终返回一个promise.resolve,这个resolve中的fn其实就是app.use中的函数,此时我们才执行了app.use中的函数,并且把ctx,next通过参数的形式穿给了这个函数;

所以中间件的声明是在app.use,执行是在我们app.listen接受到请求的时候,这样我们就可以在接受到请求的时候一次执行我们的中间件

综上就是所有内容了,如有问题或异议,恳请指出,不甚感激~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值