koa源码解析-compose函数

孤阴不生,独阳不长,负阴而抱阳,充气以为和。

前言

本文主要介绍的是koa框架是如何优雅的处理中间件。

基于koa v2.13.1、koa-compose v4.2.0

使用示例

先看一段我们很熟悉的node项目中入口文件代码:

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);
复制代码

上面的代码很简单,主要就是三个步骤:

  1. 创建一个koa对象的实例app
  2. 再调用app的use方法。以app.use(XX).use(XX)插入中间件函数。
  3. 最后调用启动方法,并监听3000端口

image.png

中间件方法的保存,与触发

1. 保存中间件

首先我们来看看是如何以app.use(fn).use(fn).use(fn)保存中间件的

//lib/application.js
module.exports = class Application extends Emitter {
  use(fn) {
    //校验fn格式
    this.middleware.push(fn);
    //返回this,可以一直app.use()重复调用
    return this;
  }  
};

复制代码

app.use(fn)做的事情很简单:就是在http服务启动前,将我们项目预设的中间件保存在middleware数组中,等待http服务发起回调触发这些中间件。

那下面我们看看一个http请求过来,保存在middleware中的中间件是如何触发的。

2. 触发中间件

创建一个http的服务server,并以callback()作为回调。

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

callbakc()方法中,以compose来组织中间件,放返回回调方法

callback() {
    //以某种方式组织这些中间件
    const fn = compose(this.middleware);

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

    //获取到http请求中的req,res,保存在ctx上下文中
    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn);
    };
    //返回经过compose处理后的中间件方法
    return handleRequest;
  }
复制代码

在接受的http请求时,作为回调函数执行经过compose处理过的中间件方法,修改上下文ctx

handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    const onerror = err => ctx.onerror(err);
    const handleResponse = () => respond(ctx);
    //将上下文作为入参传入中间件。
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }
复制代码

compose函数的实现

下面就来介绍我们的核心方法。
就是因为这个compose方法,我们才得以在中间件中以await next()的形式将控制权转交给下一个中间件,并在最后一个中间件将控制权依次向上传递。

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

  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的源码来源koa-compose,代码也比较简单。我们来简单分析一下:

  1. 做一个入参(中间件)的类型校验
  2. 并马上返回一个译名函数。做一个柯里化,分步传入参数。对应源码:
//compose化中间件
const fn = compose(this.middleware); 
...
//返回请求的回调函数-即compose之后的中间件函数
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
复制代码
  1. 在佚名函数中马上返回了一个函数调用。做了一个尾递归的优化,防止栈溢出。一句话解释:在A函数内部返回B函数的执行结果,B函数的内存空间就能马上释放
  2. 而最关键的就是dispatch函数,主要逻辑:
    1. 先做了一个边界情况 if (!fn) return Promise.resolve(),没有下一个中间件了,就Promise.resolve(),回溯到上一个中间件的await next()之后的逻辑;
    2. 然后就是返回第一个中间件的调用结果,这样就是一个尾递归优化。
    3. 关键的是fn的第二个参数,是下一个中间件函数,能够让上一个中间件优雅的调用。

小结:

  1. js控制权从第一个中间件开始,依次往下传递。向下传递的关键在于执行await next(),手动触发执行。
  2. await next()方法也为后面的控制权向上回溯做了铺垫。每一个await next()都嵌套了下一个中间件的方法,直到最后一个没有await next()的中间件执行完毕
  3. 最后一个中间件执行完毕,开始向上回溯,执行上一个中间件的await next()后的方法

总结

  1. compose函数利用了尾递归进行了内存栈的优化
  2. 将下一个中间件的作为当前执行中间件的入参,来手动控制进入下一个中间件的时机。
  3. 利用async/await来处理异步的中间件来向上回溯
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享