知识

对koa2源码的分析

2018-11-14  本文已影响251人  Djknight

最近在学习koa2,但是自己陷入了瓶颈期。就是不知道学什么好,对未来有点迷茫。还好最近看到了知乎上的狼叔的文章

感到迷茫的话就一天阅读十个npm模块。

这让我坚定了阅读源码的信念

KOA2的基本组成


  1. application.js:框架入口;负责管理中间件,以及处理请求
  2. context.js:context对象的原型,代理request与response对象上的方法和属性
  3. request.js:request对象的原型,提供请求相关的方法和属性
  4. response.js:response对象的原型,提供响应相关的方法和属性

我们主要来看看application.js。
那我们就根据自己写koa的习惯来一步步看吧!
我们一般都会先new一个Koa实例

const app = new Koa();

这样我们就先来看看它的构造函数:

// 我们就讲讲一些简单的吧!
constructor() {
    super();

    this.proxy = false;
    this.middleware = []; //中间件栈
    this.subdomainOffset = 2;
    this.env = process.env.NODE_ENV || 'development';
    this.context = Object.create(context); //Object.create(context)创建一个对象,这个对象的原型指向context
    this.request = Object.create(request);
    this.response = Object.create(response);
    // 所以上面三句话就是创造了三个对象
  }

总而言之,它的构造函数只是初始化了一些我们接下来所必须的东西。

接着我们要开始写代码了,比如

app.use(async (ctx, next) => {
  console.log(`1 start`);
  await next();
  console.log('1 end');
})

app.use(async (ctx, next) => {
  console.log(`2 start`);
  await next();
  console.log('2 end');
})

app.use(async (ctx, next) => {
  console.log(`3 start`);
  await next();
  console.log('3 end');
})

这里我们用到了use这个函数 那我们再来看看use吧

use(fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    if (isGeneratorFunction(fn)) {
      deprecate('Support for generators will been removed in v3. ' +
                'See the documentation for examples of how to convert old middleware ' +
                'https://github.com/koajs/koa/tree/v2.x#old-signature-middleware-v1x');
      fn = convert(fn);
    }
    debug('use %s', fn._name || fn.name || '-');
    this.middleware.push(fn);
    return this;
  }

可以看到use函数其实也很简单。首先进行判断,如果传入的中间件不是函数则报错,然后判断如果是generator函数则将其转化为async await类函数。具体怎么转换的我们就先不讲啦。然后再进行统一的错误管理。最后,再把这个中间件推入中间件栈。并返回app实例,以便于我们进行链式操作。

之后我们会怎么写代码呢? 当然是向下面这样

app.listen(3000);

所有准备就绪,就可以开始监听端口啦。

然后这个listen函数 则是最重要的一点!


listen() {
    debug('listen');
    const server = http.createServer(this.callback());
    return server.listen.apply(server, arguments);
  }

进行统一的错误管理,并用原生的node.js方法创建一个http服务器。而这个服务器的回调函数则是由callback()创建,那么我们很容易想到,callback应该会返回一个方法。让我们来继续看看callback()

callback() {
    const fn = compose(this.middleware);

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

    return (req, res) => {
      res.statusCode = 404;//如果接下来的中间件没有设置res 则默认为404状态
      const ctx = this.createContext(req, res); //创建ctx, 把request和response以及其它东西封装进去
      onFinished(res, ctx.onerror); // 当res结束或者报错时,调用onerror回调函数,这是一个npm库提供的方法
      fn(ctx).then(() => respond(ctx)).catch(ctx.onerror);
    };
  }

果不其然,它返回了一个方法!
接下来有两行代码最为重要,是koa2实现洋葱式调用的关键!!!
就是这两行
const fn = compose(this.middleware);
fn(ctx).then(() => respond(ctx)).catch(ctx.onerror);

compose是由一个名为koa-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
   * @return {Promise}
   * @api public
   */

  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] //fn只是一个函数声明,在下面调用
      if (i === middleware.length) fn = next 
      // 这里的next其实没有用 只是用来处理i ===middleware.length的情况 
      // next永远是空 这个next和下面的next是不一样的
      if (!fn) return Promise.resolve()
      try {
        // 因为fn()是一个async函数 返回一个promise对象 Promise.resolve()遇到promise对象的时候会原封不动的返回该对象
        // context就是封装好的ctx对象, next是你写在use里面的next
        // 执行fn()代码 就是执行自定的async函数 遇到内部await next()则会等待回调函数结束
        // 而这个回调函数递归调用下一个middleware 碰到下一个middleware的await next()则会继续调用下一个
        // 直到调用到最后一个 返回一个空的promise.resolve()对象 则先是最后一个middleware收到这个promise对象
        // 就执行await()下面的函数 最后一个中间件执行完毕后
        // 则会再到之前的中间件去执行
        return Promise.resolve(fn(context, function next () {

          /* 这个fn()就是
          next就是它的回调函数
          async (ctx,next) => {
             console.log("1-start");
             await next();
             console.log("1-end");
          } 
          注意这上面的函数是我们常写的函数 ctx就是context, next就是function next(){...}
          * fn(context, function next () {  //
              return dispatch(i + 1)
            })
          */
          return dispatch(i + 1)
        }))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

是不是很复杂鸭。。 不过没关系 大家可以跟着我的注释一步一步的看下来,相信大家肯定会懂的!。
注意我们在applicaiton.js调用 dispatch的时候并没有传入next
fn(ctx).then(() => respond(ctx)).catch(ctx.onerror);
所以,在dispatch里面的形参next一直为null!

好啦 这就是最基本的koa2实现洋葱式调用的方法啦 希望大家看得懂,看不懂的可以在留言区多多跟我讨论

上一篇下一篇

猜你喜欢

热点阅读