koa中间执行机制

start


基于 koa 2.11 按如下流程分析:git

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

const one = (ctx, next) => {
  console.log('1-Start');
  next();
  ctx.body = { text: 'one' };
  console.log('1-End');
}
const two = (ctx, next) => {
  console.log('2-Start');
  next();
  ctx.body = { text: 'two' };
  console.log('2-End');
}

const three = (ctx, next) => {
  console.log('3-Start');
  ctx.body = { text: 'three' };
  next();
  console.log('3-End');
}

app.use(one);
app.use(two);
app.use(three);

app.listen(3000);

app.use()


use 方法定义在 koa/lib/application.js 中:github

use(fn) {
  // check middleware type, must be a function
  if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
  // 兼容 generator
  if (isGeneratorFunction(fn)) {
    deprecate('Support for generators will be removed in v3. ' +
              'See the documentation for examples of how to convert old middleware ' +
              'https://github.com/koajs/koa/blob/master/docs/migration.md');
    fn = convert(fn);
  }
  debug('use %s', fn._name || fn.name || '-');
  
  // 存储中间
  this.middleware.push(fn);
  return this;
}

this.middleware数组

这就是一个数组,用来存放全部中间件,而后按顺序执行。promise

this.middleware = [];

app.listen()


这个方法定义在 koa/lib/application.js 中:闭包

listen(...args) {
  debug('listen');
  
  // 建立 http 服务并监听
  const server = http.createServer(this.callback());
  return server.listen(...args);
}

this.callback()app

callback() {
  // 处理中间件
  const fn = compose(this.middleware);

  if (!this.listenerCount('error')) this.on('error', this.onerror);

  const handleRequest = (req, res) => {
    // 建立 Context
    const ctx = this.createContext(req, res);
    // 执行中间件处理请求和响应
    return this.handleRequest(ctx, fn);
  };

  return handleRequest;
}

this.handleRequestkoa

handleRequest(ctx, fnMiddleware) {
  const res = ctx.res;
  res.statusCode = 404;
  const onerror = err => ctx.onerror(err);
  // 将响应发出的函数
  const handleResponse = () => respond(ctx);
  onFinished(res, onerror);
  // 这里会将 ctx 传给中间件进行处理,
  // 当中间件流程走完后,
  // 会执行 then 函数将响应发出
  return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}

respond(ctx)函数

function respond(ctx) {
  // 省略其余代码
  // ...
  // 发出响应
  res.end(body);
}

捋一捋流程,由上面的代码能够知道,存放中间的数组是经过 compose 方法进行处理,而后返回一个fnMiddleware函数,接着将 Context 传递给这个函数来进行处理,当fnMiddleware执行完毕后就用respond方法将响应发出。ui

compose(this.middleware)


compose 函数经过koa-compose引入:this

const compose = require('koa-compose');

compose 定义在koajs/compose/index.js

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) {
    // 这个 index 是标识上一次执行的中间件是第几个
    let index = -1
    
    // 执行第一个中间件
    return dispatch(0)
    function dispatch (i) {
      // 检查中间件是否已经执行过,
      // 举个例子,当执行第一个中间件时 dispatch(0),
      // i = 0, index = -1, 说明没有执行过,
      // 而后 index = i, 而 index 经过闭包保存,
      // 若是执行了屡次,就会报错
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      
      // 经过传入的索引从数组中获取中间件
      let fn = middleware[i]
      
      // 若是当前索引等于中间件数组的长度,
      // 说明已经中间件执行完毕,
      // fn 为 fnMiddleware(ctx) 时没有传入的第二个参数,
      // 即 fn = undefined
      if (i === middleware.length) fn = next
      // fn 为 undefined, 返回一个已经 reolved 的 promise
      if (!fn) return Promise.resolve()
      
      try {
        // 执行中间件函数并将 dispatch 做为 next 函数传入
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

结束执行流程

如今来捋一下 fnMiddleware的执行流程:

// fnMiddleware 接收两个参数
function (context, next) {
  // ....
}

// 将 context 传入,并无传入 next,
// 因此第一次执行时是没有传入 next 的
fnMiddleware(ctx).then(handleResponse).catch(onerror);

next == undefined时会结束中间件执行,流程以下:

function dispatch (i) {
  //...
  
  // 经过传入的索引从数组中获取中间件,
  // 可是由于已经执行完了全部中间件,
  // 因此当前 i 已经等于数组长度,
  // 即 fn = undefined
  let fn = middleware[i]

  // 若是当前索引等于中间件数组的长度,
  // 说明已经中间件执行完毕,
  // 又由于 fnMiddleware(ctx) 时没有传入的第二个参数 next,
  // 因此 fn = undefined
  if (i === middleware.length) fn = next
  
  // fn 为 undefined, 返回一个已经 reolved 的 promise
  // 中间件执行流程结束
  if (!fn) return Promise.resolve()
  
  // ...
}

中间件执行流程

上面先说告终束流程,如今说一下如何顺序执行,造成洋葱模型:

function dispatch (i) {
  // ...省略其余代码
  
    try {
    // 分步骤说明
    // 首先经过 bind 将 dispatch 构建为 next 函数
    const next = dispatch.bind(null, i + 1);
    // 将 ctx, next 传入执行当前中间件,
    // 当在中间件中调用 next() 时,
    // 本质上是调用 diapatch(i + 1),
    // 也就是从数组中获取下一个中间件进行执行,
    // 在这时,会中断当前中间件的执行流程转去执行下一个中间件,
    // 只有当下一个中间件执行完毕,才会恢复当前中间件的执行
    const result = fn(context, next);
    // 中间件执行完毕,返回已经 resolve 的 promise,
    // 那么上一个中间件接着执行剩下的流程,
    // 这样就造成了洋葱模型
    return Promise.resolve(result);
  } catch (err) {
    return Promise.reject(err)
  }
}

开头的例子执行结果以下:

const one = (ctx, next) => {
  console.log('1-Start');
  next();
  ctx.body = { text: 'one' };
  console.log('1-End');
}
const two = (ctx, next) => {
  console.log('2-Start');
  next();
  ctx.body = { text: 'two' };
  console.log('2-End');
}

const three = (ctx, next) => {
  console.log('3-Start');
  ctx.body = { text: 'three' };
  next();
  console.log('3-End');
}

// 1-Start
// 2-Start
// 3-Start
// 3-End
// 2-End
// 1-End
// 而 ctx.body 最终为 { text: 'one' }

next()


没有调用 next()

// 没有调用 next() 函数
app.use((ctx, next) => {
  console.log('Start');
  ctx.body = { text: 'test' };
  console.log('End');
});

由于 next 函数本质上就是经过dispatch(i + 1)来调用下一个中间件,若是没有调用 next 函数,就没法执行下一个中间件,那么就表明当前中间件流程执行结束。

屡次调用 next()

app.use((ctx, next) => {
  console.log('Start');
  ctx.body = { text: 'test' };
  // 屡次调用 next 函数
  next(); // 本质上是 dispatch(i + 1)
  next(); // 本质上是 dispatch(i + 1)
  console.log('End');
});

这里假设 nextdispatch(3),那么 index 就为 2,第一次执行 next 函数时,会发生以下逻辑:

// index == 2
// i == 3
// 不会报错
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
// 赋值后 index 为 3 了 
index = i

假设第三个中间件是最后一个中间件,那么执行完第一次 next 函数会当即执行第二个 next 函数,依然执行这个逻辑,可是 index 已经为 3 了,因此会致使报错:

// index == 3
// i == 3
// 报错
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
相关文章
相关标签/搜索