koa-compose源码学习

122 阅读4分钟

前言

什么是中间件?

中间件本质是一个函数,程序输入值到输出值之间运用中间件可以进行各种加工。

image.png

Koa 采用独特的"洋葱模型"中间件机制: 注意这个“洋葱模型”和递归执行斐波拉契数列有点类似

请求
  ↓
中间件1 (开始)
  ↓
中间件2 (开始)
  ↓
中间件3 (开始)
  ↓
中间件3 (结束)
  ↓
中间件2 (结束)
  ↓
中间件1 (结束)
  ↓
响应
// 递归执行斐波拉契数列
const fb = (n) => {
	if(n === 1 || n === 2) return 1
	return fb(n - 1) + fb(n - 2)
}

fb(10) // 5

koa-compose基础使用

思考 ?为啥arr的输出值是[1,2,3,4,5,6]

    const arr = []
    const stack = []
    
    // 中间件函数1
    stack.push(async (context, next) => {
     // arr的值为[1]
      arr.push(1)
      // 执行next函数,此时next函数里面会干什么呢 ?
      await next()
      arr.push(6)
    })
      
    // 中间件函数2
    stack.push(async (context, next) => {
    // 此时arr的值为[1, 2]
      arr.push(2)
      await next()
      arr.push(5)
    })
    
    // 中间件函数3
    stack.push(async (context, next) => {
     // 此时arr的值为[1, 2, 3]
      arr.push(3)
      // next里面已经没有中间件函数需要执行了拉,等待next执行完成后,arr.push(4)执行
      await next()
      // 此时arr的值为[1, 2, 3, 4]
      arr.push(4)
    })

    await compose(stack)({})
    console.log(arr)   // [1,2,3,4,5,6]

带着上面的疑问我们来看下面的源码解析

koa-compose源码解析

1. composeSlim函数

composeSlim是一个compose的简略函数,功能相同,用于production环境

  • a. composeSlim返回了一个async(ctx, next) => {...}函数,假设为函数A
  • b. 执行函数A,用户可以传参ctx和next给A函数
  • c. A函数内部执行dispatch(0)(),注意有2个括号,第一个括号是执行dispacth(0),将i = 0传入进去,i 被缓存到了函数作用域中,此时dispacth(0)返回一个函数async () => {...},假设为函数B,第二个()就是执行该B函数
  • d. 此时js执行栈进入B函数中执行,B函数重点是获取fn函数并执行它。如果middleware[i]存在值,fn等于middleware[i], 否则为next函数, 当fn为空时,请阅读 f 点解析。
  • e fn函数被执行,fn函数就是用户写的中间件函数,这里的重点是fn会将dispatch(i+1)作为传参,也就是我们上文中“koa-compose基础使用”示例中的next函数,用户在中间件函数fn内部手动调用next函数,next函数执行时又拿到下一个中间件函数,下一个被执行,继续传参next函数,形成了套娃...直到fn为空,看接下来的 f 点解析
  • f. 当fn为空时,函数return,最后一个中间件函数体中的await next()就执行完成,参照“koa-compose基础使用”示例中间件函数3,然后执行arr.push(4), 至此中间件函数3全部执行完成,然后中间件函数2的await next()也就执行完成了....以此类推,所有中间件函数执行完成
/**
 * @param {Array} middleware 中间件函数数组
 * @return {Function}
 */
const composeSlim = (middleware) => async (ctx, next) => {
  const dispatch = (i) => async () => {
    const fn = i === middleware.length
      ? next
      : middleware[i]
    if (!fn) return
    return await fn(ctx, dispatch(i + 1))
  }
  return dispatch(0)()
}

/** @typedef {import("koa").Middleware} Middleware */

/**
 * Compose `middleware` returning
 * a fully valid middleware comprised
 * of all those which are passed.
 *
 * @param {...(Middleware | Middleware[])} middleware 中间件数组
 * @return {Middleware}
 */

const compose = (...middleware) => {
  const funcs = middleware.flat()
  
  // 遍历传参,保证传参都是数组
  for (const fn of funcs) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }

  if (process.env.NODE_ENV === 'production') return composeSlim(funcs)
  ....
}

2. compose函数

和上文composeSlim函数一样的逻辑思路

const compose = (...middleware) => {
  // 将传参拍平
  const funcs = middleware.flat()

  for (const fn of funcs) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }

  if (process.env.NODE_ENV === 'production') return composeSlim(funcs)
  
  // 返回一个函数
  return async (ctx, next) => {
    const dispatch = async (i) => {
      // 取出下一个执行的函数fn
      const fn = i === funcs.length
        ? next
        : funcs[i]
      if (!fn) return

      let nextCalled = false
      let nextResolved = false
      /**
       * 异步函数,用于执行下一步操作
       *
       * @returns 返回下一步操作的结果
       * @throws 当next()被多次调用时,抛出错误
       */
      const nextProxy = async () => {
        // nextCalled是确保next()函数只被调用一次
        if (nextCalled) throw Error('next() called multiple times')
        nextCalled = true
        try {
          // 执行dispatch(i + 1)
          return await dispatch(i + 1)
        } finally {
          nextResolved = true
        }
      }
      const result = await fn(ctx, nextProxy)
      if (nextCalled && !nextResolved) {
        throw Error(
          'Middleware resolved before downstream.\n\tYou are probably missing an await or return'
        )
      }
      return result
    }
    
    // 执行dispatch(0)
    return dispatch(0)
  }
}

用promise如何实现示例效果

koa-compose基础使用示例的结构就类似于如下代码
M1的resolved状态依赖M2的resolved状态,M2依赖M3... 等最里面的resolved之后,依次往外传递。和洋葱好像~

const arr = []
const fun1 = () => new Promise((resolve) => resolve(1))
const fun2 = () => new Promise((resolve) => resolve(2))
const fun3 = () => new Promise((resolve) => resolve(3))
fun1().then(() => {  // M1
  arr.push(1)
  return fun2().then(() => { // M2
    arr.push(2)
    return fun3().then(() => { // M3
      arr.push(3)
    }).then(() => { // M4
      arr.push(4)
    })
  }).then(() => { // M5
    arr.push(5)
  })
}).then(() => { //M6
  arr.push(6)
  console.log(arr) // [1,2,3,4,5,6]
})

总结

之前对中间件的认识都是模糊的状态,通过阅读源码终于理清楚啦,好开心~
如有问题,欢迎指正~