源码系列—koa-compose

271 阅读2分钟

这是我参与更文挑战的第11天,活动详情查看:更文挑战

koa-compose 是用于将 Koa 中间件进行合并的工具,是实现 Koa 中间件洋葱模型的核心代码。

Koa 简单介绍

在分析 koa-compose 源码之前,先了解一下 koa 的相关知识。

Koa 应用程序是一个包含一组中间件函数的对象,它是按照类似堆栈的方式组织和执行的。中间件以 async 函数的形式,通过 use 方法添加。

当请求开始时,请求流会优先通过第一个中间件,当该中间件调用 next() 则该函数暂停并将控制传递给定义的下一个中间件。当没有下一个中间件时,堆栈将展开并且每个中间件恢复执行其上游行为。

这种中间件模型叫做「洋葱模型」,靠前的中间件处于「洋葱」的外层,靠后的中间件则处于「洋葱」的里层,如下图所示:

示例代码如下:

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

app.use(async (ctx, next) => {
    console.log('first begin')
    next();
    console.log('first end')
})

app.use(async (ctx, next) => {
    console.log('second begin')
    next();
    console.log('second end')
})

app.use((ctx, next) => {
    console.log('third begin')
    next();
    console.log('third end')
})

app.use(ctx => {
    console.log('response');
    ctx.body = 'hello'
})

app.listen(3000)
// 运行后,访问 localhost:3000,结果如下:
// first begin
// second begin
// third begin
// response
// third end
// second end
// first end

简单看一下 Koa 的源码,它是定义了一个 middleware 数组用来存放所有的中间件,然后通过 koa-composemiddleware 中的所有中间件组成洋葱模型的形式,返回一个大的中间件。相关代码如下所示:

module.exports = class Application extends Emitter {
  constructor(options) {
    // ......
    this.middleware = []; // 初始化一个中间件数组
    //...
  }
    
  use(fn) {
    // ......
    this.middleware.push(fn); // 将中间件函数放入数组中
    // .....
  }
  
  callback() {
    // 使用koa-compose 将中间件们以洋葱模型的方式组合
    const fn = compose(this.middleware); 
    //......
  }
  
  listen(...args) {
    debug('listen');
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }
}

koa-compose 源码

koa-compose 代码量非常少,核心代码只有十来行,源码及解释如下所示:

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) {
    // 用于确保 next 只调用一次
    let index = -1
    return dispatch(0) // 返回一个 Promise.resolve(fn0(context, ...))
    function dispatch (i) {
      // 第一次 next 递归完后,index === middleware.length
      // 通过 i 与 index 比较确保中间件只调用了一次 next
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      let fn = middleware[i]
      // 如果是最后一个中间件,就把参数 next 放到最后
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {
        // 递归调用 dispatch,通过 bind 绑定参数
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

可以看到,koa-compose是借助函数dispatch的递归跟promise.resolve()方法,将每一个中间件都封装到Promise.resolve()然后传递到上一个中间件的 next 方法中,通过层层嵌套实现了 Koa 中间件的洋葱模型。

回过头来看开篇的那个简单的示例,经过 compose 方法转换后的结构如下所示:

return function (context, next) {
    // dispatch(0) 返回 Promise.resolve(中间件1)
    Promise.resolve(async (context, dispatch.bind(null, 1)) => {
        console.log('second begin')
       	
        // next(), 即 dispatch.bind(null,1)(),返回 Promise.resolve(中间件2)
        Promise.resolve(async (context, dispatch.bind(null, 2)) => {
            console.log('second begin')
            
            // next(),即 dispatch.bind(null, 2)(),返回 Promise.resolve(中间件3)
            Promise.resolve(async (context, dispatch.bind(null, 3)) => {
                console.log('third begin')
                
                // next(),即 dispatch.bind(null, 3)(), 返回 Promise.resolve(中间件4)
                Promise.resolve(async (context) => {
                    console.log('response');
                    context.body = 'hello'
                })
  
                console.log('third end')
            })
    
            console.log('second end')
        })
        
        console.log('second end')
    })
}

资料

koa-compose 源码

Egg.js 与 Koa