Koa引用库之Koa-compose

概述

compose 是一个工具函数,Koa.js 的中间件经过这个工具函数组合后,按 app.use() 的顺序同步执行,也就是造成了 洋葱圈 式的调用。javascript

这个函数的源代码不长,不到50行,代码地址 github.com/koajs/compo…java

利用递归实现了 Promise 的链式执行,无论中间件中是同步仍是异步都经过 Promise 转成异步链式执行。node

源码解读

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!')
  }
  ...
}复制代码

函数开头对参数作了类型的判断,确保输入的正确性。middleware 必须是一个数组,数组中的元素必须是 functiongit

function compose (middleware) {
  //...

  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, function next () {
          return dispatch(i + 1)
        }))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}复制代码

接下来,是返回了一个函数,接受两个参数,contextnextcontextkoa 中的 ctxnext 是全部中间件执行完后,框架使用者来最后处理请求和返回的回调函数。同时函数是一个闭包函数,存储了全部的中间件,经过递归的方式不断的运行中间件。github

经过代码能够看到,做为中间件一样必须接受两个参数, contextnext。若是某个中间件没有调用 next() , 后面的中间件是不会执行的。这是很是常见的将多个异步函数转为同步的处理方式。shell

Middleware函数的写法

直接看代码:数组

const compose = require('./compose')

function mw1 (context, next) {
  console.log('===== middleware 1 =====')
  console.log(context)
  setTimeout(() => {
    console.log(`inner: ${context}`)
    next()
  }, 1000)
}

function mw2 (context, next) {
  console.log('===== middleware 2 =====')
  console.log(context)
  next()
}

function mw3 (context, next) {
  console.log('===== middleware 3 =====')
  console.log(context)
  setTimeout(() => {
    console.log(`inner: ${context}`)
  }, 1000)
  next()
}

const run = compose([mw1, mw2, mw3])

run('context', function () {
  console.log('all middleware done!')
})复制代码

输出结果是:闭包

===== middleware 1 =====
context
inner: context
===== middleware 2 =====
context
===== middleware 3 =====
context
all middleware done!
inner: context复制代码

第三个中间件中,故意把 next() 写在了异步的外面,会致使中间件还完成就直接进入下一个中间件的运行了(这里是全部中间件运行完后的回调函数)。compose() 生成的函数是 thenable 函数,咱们改一下最后的运行部分。架构

run('context').then(() => {
  console.log('all middleware done!')
})复制代码

结果是:app

===== middleware 1 =====
context
all middleware done!
inner: context
===== middleware 2 =====
context
===== middleware 3 =====
context
inner: context复制代码

看起来结果不符合咱们的预期,这是由于在 compose 源代码中,中间件执行完后返回的是一个 Promise 对象,若是咱们在 Promise 中再使用异步函数而且不使用then 来处理异步流程,显然是不合理的,咱们能够改一下上面的中间件代码。

function mw1 (context, next) {
  console.log('===== middleware 1 =====')
  console.log(context)
  return new Promise(resolve => {
    setTimeout(() => {
      console.log(`inner: ${context}`)
      resolve()
    }, 1000)
  }).then(() => {
    return next ()
  })
}

function mw2 (context, next) {
  console.log('===== middleware 2 =====')
  console.log(context)
  return next()
}

function mw3 (context, next) {
  console.log('===== middleware 3 =====')
  console.log(context)
  return new Promise(resolve => {
    setTimeout(() => {
      console.log(`inner: ${context}`)
      resolve()
    }, 1000)
  }).then(() => {
    return next ()
  })
}复制代码

输出:

===== middleware 1 =====
context
inner: context
===== middleware 2 =====
context
===== middleware 3 =====
context
inner: context
all middleware done!复制代码

这下没问题了,每个中间件都会返回一个 thenablePromise 对象。

既然是在研究Koa.js 那么咱们就把上面的代码再改改,使用 async/await 改写一下,把异步函数改为一个 thenable 函数。

async function sleep (context) {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log(`inner: ${context}`)
      resolve()
    }, 1000)
  })
}

async function mw1 (context, next) {
  console.log('===== middleware 1 =====')
  console.log(context)
  await sleep(context)
  await next()  
}

async function mw2 (context, next) {
  console.log('===== middleware 2 =====')
  console.log(context)
  return next()
}

async function mw3 (context, next) {
  console.log('===== middleware 3 =====')
  console.log(context)
  await sleep(context)
  await next ()
}复制代码

应用场景

在平常的开发中,Node 后台通常是做为微服务架构中的一个面向终端的 API Gateway。
如今有这样一个场景:咱们从三个其余微服务中获取数据再聚合成一个 HTTP API,若是三个服务提供的 service 没有依赖的话,这种状况比较简单,用 Promise.all() 就能够实现,代码以下:

function service1 () {
  return new Promise((resolve, reject) => {
    resolve(1)
  })
}

function service2 () {
  return new Promise((resolve, reject) => {
    resolve(2)
  })
}

function service3 () {
  return new Promise((resolve, reject) => {
    resolve(3)
  })
}

Promise.all([service1(), service2(), service3()])
  .then(res => {
    console.log(res)
  })复制代码

那若是 service2 的请求参数依赖 service1 返回的结果, service3 的请求参数又依赖于 Service2 返回的结果,那就得将一系列的异步请求转成同步请求,compose 就能够发挥其做用了,固然用 Promise 的链式调用也是能够实现的,可是代码耦合度高,不利于后期维护和代码修改,若是 一、二、3 的顺序调换一下,代码改动就比较大了,另外耦合度过高的代码不利于单元测试,这里有一个文章是经过依赖注入的方式解耦模块,保持模块的独立性,便于模块的单元测试。

总结

Compose 是一种基于 Promise 的流程控制方式,能够经过这种方式对异步流程同步化,解决以前的嵌套回调和 Promise 链式耦合。

Promise 的流程控制有不少种,下篇文章再来写不一样应用场景中分别运用的方法。

相关文章
相关标签/搜索