美文网首页知识
对koa2源码的分析

对koa2源码的分析

作者: Djknight | 来源:发表于2018-11-14 19:57 被阅读251次

    最近在学习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实现洋葱式调用的方法啦 希望大家看得懂,看不懂的可以在留言区多多跟我讨论

    相关文章

      网友评论

        本文标题:对koa2源码的分析

        本文链接:https://www.haomeiwen.com/subject/jnkrfqtx.html