[源码共读] 第四期:Koa co 源码解析

196 阅读4分钟

Koa源码解析

koa框架总结:主要就是四个核心概念,洋葱模型(把中间件串联起来),http请求上下文(context)、http请求对象、http响应对象。

koa的中间件机制关键在于洋葱模型和app的实例上下文(context

中间件本质

中间件本质是一个函数,这个函数传入两个参数,参数一是实例上下文(context),参数二是一个next方法

参数一 context:

context上挂载了koa实例上的属性,会传给每个中间件方法,使得中间件共享这个属性。然后编写一个增强中间件一般也是给这个context属性进行增添属性和方法。

参数二 next:

这个方法是把当前中间件的执行权给到下一个中间件,当下一个中间件函数执行完才返回执行权。

洋葱模型执行顺序:

洋葱模型实现原理

上文gif图的原理是怎么实现的呢?关键在于compose函数

compose函数,其传入一个数组,返回一个函数fn,这个函数fn返回一个Promise实例对象;

在koa源码中,koa将所有的中间件函数存放到middleware数组里,然后传给compose函数。

而这个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!')
  }

  /**
   * @param {Object} context  参数还能传一个中间件函数执行完之后的回调 next
   * @return {Promise} --返回一个promise对象
   * @api public
   */
  return function (context, next) {
    // last called middleware #
    let index = -1
    return dispatch(0)
      //洋葱模型的原理在于这个dispatch函数
    function dispatch (i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      let fn = middleware[i]
      //这种情况表示已经把所有的中间件函数都处理了,如果有next回调,则把next回调传出执行
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {
          //执行中间件函数,并把返回值抛出
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

洋葱模型的原理在于这个dispatch函数,通过控制变量i来控制哪个中间件函数调用,当调用中间件函数参数中next方法的时候,其实是调用dispatch(i+1)方法,既是在当前的函数执行上下文中调用下一个中间件函数。当下一个中间件函数执行完,就继续执行当前中间件函数,如果不调用next方法,则无法执行下一个中间件函数。

相当于:

// 这样就可能更好理解了。 这段代码若川写的
// simpleKoaCompose
const [fn1, fn2, fn3] = this.middleware;
const fnMiddleware = function(context){
    return Promise.resolve(
      fn1(context, function next(){
        return Promise.resolve(
          fn2(context, function next(){
              return Promise.resolve(
                  fn3(context, function next(){
                    return Promise.resolve();
                  })
              )
          })
        )
    })
  );
};
fnMiddleware(ctx).then(handleResponse).catch(onerror);

这种写法,让人直喊:妙啊~

Koa.prototype.handleRequest源码

compose函数返回的fn将在Koa.prototype.handleRequest方法中被调用,并通过onerror方法捕获异常

handleRequest(ctx, fnMiddleware) {
   // fnMiddleware 就是compose函数返回的fn方法
  const res = ctx.res;
  res.statusCode = 404;
  const onerror = err => ctx.onerror(err);
  const handleResponse = () => respond(ctx);
  onFinished(res, onerror);
  return fnMiddleware(ctx) //这里生成一个promise对象,当promise的状态为fulfilled时,中间件函数以及执行。
  .then(handleResponse).catch(onerror);//中间件执行期间抛出的异常在这里捕获
}
onerror(err) {
  if (!(err instanceof Error)) throw new TypeError(util.format('non-error thrown: %j', err));

  if (404 == err.status || err.expose) return;
  if (this.silent) return;

  const msg = err.stack || err.toString();
  console.error();
  console.error(msg.replace(/^/gm, '  '));
  console.error();
}

Koa.prototype.use

在koa中,通过use方法,既

app.useasync(ctx,next)=>{
//编写中间件函数
})

这个方法给app实例增加中间件。

源码

use(fn) {
  if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
  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;
}

使用这个app.use时有一层判断,是否是generator函数,如果是则用koa-convert暴露的方法convert来转换重新赋值,再存入middleware,后续再使用。

而app.use方法就是将参数中的中间件函数放到this.middleware这个数组里保存。

koa-convert源码:

function convert (mw) {
  if (typeof mw !== 'function') {
    throw new TypeError('middleware must be a function')
  }
  if (mw.constructor.name !== 'GeneratorFunction') {
    // assume it's Promise-based middleware
    return mw
  }
  const converted = function (ctx, next) {
    return co.call(ctx, mw.call(ctx, createGenerator(next)))
  }
  converted._name = mw._name || mw.name
  return converted
}

转换是通过co函数转换的。

co函数

手写简易版的co函数

简易版就写了个大概,各种错误情况没有考虑

function request(ms = 1000) {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve('lalala');
        }, ms);
    });
}

function* generatorFunc(suffix = ''){
    const res = yield request();
    console.log(res, 'generatorFunc-res' + suffix);

    const res2 = yield request();
    console.log(res2, 'generatorFunc-res-2' + suffix);

    const res3 = yield request();
    console.log(res3, 'generatorFunc-res-3' + suffix);

    const res4 = yield request();
    console.log(res4, 'generatorFunc-res-4' + suffix);
}
function fulfilled(gen,resolve,returnValue) {
    let ret=gen.next(returnValue)
    if(ret.done){
        resolve(ret.value)
        return
    }
    let promise =ret.value;
    promise.then(value => {
        fulfilled(gen,resolve,value)
    })
}
function autoGeneratorFunc( generatorFunc,...args) {

   let gen=generatorFunc(args);

   return new Promise((resolve,reject)=>{
        fulfilled(gen,resolve)
   });

}
autoGeneratorFunc(generatorFunc, '哈哈哈')

co函数源码

源码中考虑了各种情况,健壮性特别好

function co(gen) {
  var ctx = this;
  var args = slice.call(arguments, 1)

  // we wrap everything in a promise to avoid promise chaining,
  // which leads to memory leak errors.
  // see https://github.com/tj/co/issues/180
  return new Promise(function(resolve, reject) {
    // 把参数传递给gen函数并执行
    if (typeof gen === 'function') gen = gen.apply(ctx, args);
    // 如果不是函数 直接返回
    if (!gen || typeof gen.next !== 'function') return resolve(gen);

    onFulfilled();

    /**
     * @param {Mixed} res
     * @return {Promise}
     * @api private
     */

    function onFulfilled(res) {
      var ret;
      try {
        ret = gen.next(res);
      } catch (e) {
        return reject(e);
      }
      next(ret);
    }

    /**
     * @param {Error} err
     * @return {Promise}
     * @api private
     */

    function onRejected(err) {
      var ret;
      try {
        ret = gen.throw(err);
      } catch (e) {
        return reject(e);
      }
      next(ret);
    }

    /**
     * Get the next value in the generator,
     * return a promise.
     *
     * @param {Object} ret
     * @return {Promise}
     * @api private
     */

    // 反复执行调用自己
    function next(ret) {
      // 检查当前是否为 Generator 函数的最后一步,如果是就返回
      if (ret.done) return resolve(ret.value);
      // 确保返回值是promise对象。
      var value = toPromise.call(ctx, ret.value);
      // 使用 then 方法,为返回值加上回调函数,然后通过 onFulfilled 函数再次调用 next 函数。
      if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
      // 在参数不符合要求的情况下(参数非 Thunk 函数和 Promise 对象),将 Promise 对象的状态改为 rejected,从而终止执行。
      return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
        + 'but the following object was passed: "' + String(ret.value) + '"'));
    }
  });
}

参考

作者:若川
链接:juejin.cn/post/684490…
来源:稀土掘金

作者:阮一峰

链接:es6.ruanyifeng.com/#docs/gener…