深入理解 Koa 中间件之 “ 洋葱模型 ”

3 篇文章 3 订阅
1 篇文章 0 订阅

欢迎关注我的公众号『 前端我废了 』,查看更多文章!!!

前言

我们知道创建一个 Koa 应用主要分三步:

const Koa = require('koa');
// 1. 创建一个 Koa 实例
const app = new Koa();

// 2,加载多个中间件
app.use(/*中间件*/);
// ... 其他中间件

// 3. 指定服务器端口,创建一个 http 服务器
app.listen(3000);

那么中间件的执行顺序是怎样的呢?执行顺序是如何生成的呢?这篇文章就让我们来一探究竟。

koa 中间件的执行顺序

如下示例代码,例如使用中间件 x-response-timeloggerresponse

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

// 中间件1 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`);
});

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

// 中间件3 response
app.use(async ctx => {
  ctx.body = 'Hello World';
});

app.listen(3000);

上面代码中间件的执行顺序如下动图
中间件执行顺序

以上中间件的执行顺序,我们会常听到一个词叫做 “洋葱模型”(如下图),何为“洋葱模型“?洋葱内的每一层表示一个独立的中间件,用于实现不同的功能,比如日志记录,异常处理等。每次请求都会从左侧最外层开始,一层层经过中间件,当执行到最里层的中间件之后,接着从最里层的中间件开始逐层返回。因此对于每层的中间件来说,在一个 请求和响应 周期中,都有两个时机点来添加不同的处理逻辑。是不是有点像 DOM 事件流的事件捕获阶段(从外到里)和事件冒泡阶段(从里到外)。

洋葱模型

上面中间件的执行顺序是怎么生成的呢?从 Koa 源码入手,从 Koa 源码的 `package.json 中 main 字段得知入口文件指向的是 lib/application.js 文件,详解一下创建一个 koa 应用执行过程:

  1. const app = new Koa() ;我们 new Koa() 其实就是创建类 Application 的实例;
  2. app.use(/*中间件*/);当我们调用 app.use(function) 加载中间件时,use 函数内部将中间件函数都保存到了 middleware 数组里面;
  3. app.listen(/* 端口 */),内部使用 http.createServer() 创建一个 http 服务器,并调用 this.callback() 的返回值作为参数;callback 函数内部调用 compose 函数(即 koa-compose 中间件),就是造就中间件执行顺序的"幕后黑手"。

下面我们逐行解析 koa-compose 源码,看看是如何生成 “洋葱模型”的。

koa-compose 源码逐行详解

koa-compose 源码(index.js 文件)内部导出的就是 compose 函数,我们除了看源码,也可以结合对应测试用例(test.js 文件)来理解。

/**
 * compose 函数接收一个中间件数组,返回一个中间件函数
 * @param {Array} middleware
 * @return {Function}
 */
function compose (middleware) {
  // 若传入的参数 middleware 不是数组,则抛错
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  // 若 middleware 数组项不是函数,则抛错
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }

  /**
   * 返回一个中间件函数,第一参数为一个请求的上下文对象;第二个参数 next 函数为调用下一个中间件的函数
   */
  return function (context, next) {
    // 当前中间件在 middleware 数组中的索引位置
    let index = -1
    // 返回 dispatch(0) 结果
    return dispatch(0)
    
    // 该函数目的递归调用中间件,生成中间件嵌套调用结构
    function dispatch (i) {
      // 每个中间件函数内部只能调用一次 next 函数,否则抛错
      // 测试用例 should throw if next() is called multiple times 
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      // 记录索引
      index = i
      // 取出当前中间件函数
      let fn = middleware[i]
      // 若当前索引值等于中间件数组的长度,即 middleware 数组的中间件都处理完了,则 fn 赋值为参数 next
      if (i === middleware.length) fn = next
      // 若 fn 不存在,则 resolve 掉,结束
      if (!fn) return Promise.resolve()
      try {
        // 返回 Promise,执行中间件函数 fn,这里将下一个中间件函数执行器作为第二个参数传入当前中间件,目的是将下一个中间件函数的执行权交由当前中间件,在其内部手动调用;
        // 这里利用 bind 函数来实现,bind 函数执行后会返回一个新的函数,并不会立即执行,什么时候执行呢?也就是当前中间件 fn 函数里的 await next() 执行时,此时这个 next 函数也就是现在 fn 函数传入的第二个参数 dispatch.bind(null, (i + 1)的返回值
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

假如有三个中间件分别为 fn1fn2fn3,那么在 Koa 内部经过中间件 koa-compose 组合后,将会生成如下的嵌套结构

// compose 函数返回值也是一个中间件函数
const fn = compose([fn1, fn2, fn3])
// compose 返回的函数 fn 内部嵌套结构,简略代码
function(context){
    return Promise.resolve(
      fn1(context, function next(){
        return Promise.resolve(
          fn2(context, function next(){
              return Promise.resolve(
                  fn3(context, function next(){
                    return Promise.resolve();
                  })
              )
          })
        )
    })
  );
};

以上就是生成中间件执行顺序的代码,核心就是 dispatch 函数,通过递归生成中间件嵌套调用结构,将下一个中间件的控制权通过函数参数的形式传递给当前中间件,以此类推,生成类似洋葱模型结构的执行顺序。

扩展:Koa 与 Express 对比

  1. express 拥有路由、模板等框架常见功能,Koa 不含任何中间件,Koa 可被视为 node.js 的 http 模块的抽象,Express 则是 node.js 的应用程序框架。

  2. 中间件实现机制;express 基于 Callback,koa 基于 Promise

  3. 错误处理;express 对错捕获处理起来很不友好,每一个回调都拥有一个新的调用栈,因此你没法对一个 callback 做 try catch 捕获,你需要在 Callback 里做错误捕获,然后一层一层向外传递。

  4. 响应机制;express在调用 res.send 方法后就立即响应了,而koa则是在所有中间件调用完成之后,在最外层中间件进行响应。

    引用一段其他网友总结的 xpress 和 koa 中间件机制的不同:

    其实中间件执行逻辑没有什么特别的不同,都是依赖函数调用栈的执行顺序,抬杠一点讲都可以叫做洋葱模型。Koa 依靠 async/await(generator + co)让异步操作可以变成同步写法,更好理解。最关键的不是这些中间的执行顺序,而是响应的时机,Express 使用 res.end() 是立即返回,这样想要做出些响应前的操作变得比较麻烦;而 Koa 是在所有中间件中使用 ctx.body 设置响应数据,但是并不立即响应,而是在所有中间件执行结束后,再调用 res.end(ctx.body) 进行响应,这样就为响应前的操作预留了空间,所以是请求与响应都在最外层,中间件处理是一层层进行,所以被理解成洋葱模型,个人拙见。

总结

牛逼!!!

参考

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值