koa源码分析(三)中间件

中间件的作用

大部分人用koa应该是用来实现后端服务的。后端服务最常见的就是实现接口了。但这些接口一般有一些相同的功能。例如日志打印请求时间,请求参数等。每写一个接口都写这些功能,不仅浪费时间,代码也难看。因此将这些通用功能提取成中间件,请求来了之后,经过各个中间件进行处理。

存储中间件

首先中间件是由我们,koa的用户定义的。所以koa要将我们定义的中间件存储起来。我们应该都写过中间件,知道每个中间件其实就是一个方法。然后中间件的执行是有顺序的。所以要存储一些有顺序的方法,最简单的就是使用一个数组。添加一个中间件就是为数组push一个方法。
我们使用过koa,都知道koa添加中间件的方式是app.use(fn)。app就是koa模块的实例。

class Koa {
  constructor(){
    this.middlewares = []
  }
  use(fn){
    this.middlewares.push(fn)
  }
}
const app = new Koa()
app.use((ctx, next) => {})
console.log(app.middlewares.length)
console.log(app.middlewares[0].toString())

稍作思考,我们就知道让数组middlewares作为app的一个属性,为app提供一个方法use,功能就是为middlewares数组push一个方法。上面的代码会在控制台上打印出1后打印出(ctx,next)=>{},这里就不贴图了。

中间件执行顺序

现在我们已经能够存储中间件了,那么如何让他们按照一定的顺序去执行呢?最简单的就是链式执行了。就是一个执行完之后去执行另一个。代码也很简单。

function sleep(time) {
  return new Promise((resolve)=> {
    setTimeout(()=>{
      resolve()
    }, time)
  })
}
let middleWare0 = async function (ctx, next) {
  let startTime = Date.now()
  if(next){
    await next()
  }
  console.log(Date.now() - startTime)
}
let middleWare1 = async function (ctx, next) {
  if(next){
    await next()
  }
  await sleep(1000)
}
async function compose(middlewares, ctx) {
  for(let middleware of middlewares){
    await middleware(ctx)
  }
}
compose([middleWare0, middleWare1], {})

compose方法就是用来链式执行中间件的。sleep函数就是用来模拟异步操作的。一共有两个中间件。确实是先执行了中间件0,然后执行了中间件1。这时应该能看出上述代码有些问题:
首先,我们在平常写代码的时候是没有if(next)这个判断的。这里去掉这个判断就会报错。因为在执行中间件的时候根本没有给他next这个参数。next是undefined,而不是一个函数,所以会报错。
其次,middleWare0的代码功能其实是想等待后面的中间件执行完之后再console.log,但现在确是立刻就输出了。时间差也是0毫秒左右。而我们要等待middleWare1完成,应该是有1000ms左右。
**koa要实现的中间件不是简单的链式执行。而是前面的中间件能够控制后面的中间件该何时执行。**很高端吧。其实,不用害怕。中间件只不过是一个函数罢了,而他想要控制另一个函数该如何执行,最简单就是把这个函数告诉他,也就是作为一个参数传递进去。我们把compose改一改 。

let compose = async function (ctx) {
  await middlewares[0](ctx,middlewares[1])
}

首先我们考虑只有两个中间件的情况。上述代码执行了中间件0,并将ctx和中间件1传给了它。也就是在中间件0中的next就是中间件1。这和我们熟悉的不一样,next在执行时并不需要我们传参数。这时我们可以推断出next这个函数应该是这种形式:

next[i] = async function(){
    await middlewares[i](ctx,next[i+1])
}

next[i]应该返回的是一个异步函数,里面执行了当前第i个中间件,并将ctx,以及next[i+1]传递给第i个中间件。
不难发现,这是一个递归的结构。还有对比上面两段代码。其实结构是一样的。所以最终能够写出这段代码:

function compose(ctx, i) {
  return async function () {
    if(middlewares[i]){
      await middlewares[i](ctx, compose(ctx, i+1))
    }
  }
}

执行compose(ctx,i)便能得到一个异步函数fn。fn的第一步就是判断中间件0是否存在,如果存在就调用中间件0,第一个参数为ctx,第二个参数为compose(ctx,i+1)这个函数的执行结果,也就是下一个中间件对应的异步函数。下面举例看下fn。

let fn = compose(ctx, 0)
fn = async function () {
  await middlewares[0](ctx, async function () {
    await middlewares[1](ctx, async function () {
      await middlewares[2](ctx, async function () {
        ...
      })
    })
  })
}

fn的格式就是上面那样(少了判断中间件是否存在)。
通过这种方式来执行中间件,每个中间件都能控制什么时候来执行下一个中间件。
不过我们还是少了一步,就是如何把middlewares这个数组传递进来。我的实现方式不是很好,但看起来比较简单。

let middlewares = []
let setMiddleWare = function (middlewaresArg) {
  middlewares = middlewaresArg
}
function compose(ctx, i) {
  return async function () {
    if(middlewares[i]){
      await middlewares[i](ctx, compose(ctx, i+1))
    }
  }
}
exports.setMiddleWare = setMiddleWare
exports.compose = compose

测试代码如下:

function sleep(time) {
  return new Promise((resolve)=> {
    setTimeout(()=>{
      resolve()
    }, time)
  })
}
let middleWare0 = async function (ctx, next) {
  let startTime = Date.now()
  await next()
  console.log(Date.now() - startTime)
}
let middleWare1 = async function (ctx, next) {
  next()
  await sleep(1000)
  console.log('中间件1')
}
let middleWare2 = async function (ctx, next) {
  await sleep(2000)
  await next()
  console.log('中间件2')
}
const compose = require('./compose')
compose.setMiddleWare([middleWare0, middleWare1, middleWare2])
let fn = compose.compose({},0)
fn()
  .catch(err => {
    console.log(err)
  })

compose便是上面的模块。若代码没有问题,则应该先执行中间件0的await next()前面的代码,然后执行中间件1,中间件1第一句就是next()也就是异步执行中间件2,这时进入中间件2,中间件2第一句会执行等待定时器2秒。这时回到中间件1,中间1会等待定时器1秒,约一秒后中间件1定时完成,输出“中间件1”。中间件0等待中间件1执行完成,输出时间差,约为1000。约1秒后,中间件2定时完成,输出“中间件2”。
下面是代码的输出结果。
输入图片说明
具体的等待时间可以自己运行验证下。
到现在为止我们基本完成了中间件的所有实现代码。那么让我们看下koa是如何实现的吧。

koa的中间件实现

//以下代码均去掉了部分无关代码
class Application extends Emitter {
  constructor() {
    super();
    this.middleware = [];
  }
  use(fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    debug('use %s', fn._name || fn.name || '-');
    this.middleware.push(fn);
    return this;
  }
  callback() {
    const fn = compose(this.middleware);
    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn);
    };
    return handleRequest;
  }
  handleRequest(ctx, fnMiddleware) {
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }
}

这时看use和callback方法是不是有一些豁然开朗了?
koa的use和我们写的相比,主要就是多了一个对入参fn的校验。 compose中,middleware数组的传入方式不同,他是直接传进了compose。让我们看下koa的compose是如何保存middleware的。

function compose (middleware) {
 //去掉了校验代码
  return function (context, next) {
    let index = -1
    return dispatch(0)
    function dispatch (i) {
      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)
      }
    }
  }
}

koa保存middleware其实用了闭包。compose(middleware)返回了一个function,而这个function用到了middleware。所以只要外面引用这个function,middleware就会一直在这个作用域存在。
compose执行后,返回的function支持两个参数,一个是context,一个是next。next为middleware数组后面的中间件。看下代码中的fn,fn随着i的增加,而取值为middleware[i],当i为middleware的长度时,也就是没有这个中间件时,fn取值为传入的next,所以传入的next可以理解为next[middleware.length] 。
接着看,确定好了fn后,执行了fn,传入了参数context,以及next[i+1],和我们刚刚的代码是一样的。不过由于他用的是promise,所以外面包裹了一层Promise.resolve。方便后面的promise链调用。而我们是用的async,功能都是差不多的。

猜你喜欢

转载自my.oschina.net/u/3361610/blog/1785551