koa源码分析

240 阅读3分钟
  • koa的核心源码只有4个文件

    • application.js
    • context.js
    • request.js
    • response.js
  • 看下koa的基础用法,新建app.js

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

  app.use(async (ctx, next) => {
    console.log('111')
    await next()
    console.log('666')
  })

  app.use(async (ctx, next) => {
    console.log('222')
    await next()
    console.log('555')
  })

  app.use(async (ctx, next) => {
    console.log('333')
    await next();
    console.log('444')
  })

  let server = app.listen(3000, () => {
    console.log('success in 3000')
  })
  • 依次输出 111 222 333 444 555 666
  • 这就是koa的洋葱模型
  • 分析application.js,这是入口文件

    • 上面代码中使用了new Koa(),知道是个class
    • 首先初始化
      constructor(options) {
        super();
        options = options || {};
        this.proxy = options.proxy || false;  // 是否代理
        this.subdomainOffset = options.subdomainOffset || 2; // 子域名偏移位置
        this.proxyIpHeader = options.proxyIpHeader || 'X-Forwarded-For';
        this.maxIpsCount = options.maxIpsCount || 0;
        this.env = options.env || process.env.NODE_ENV || 'development'; // 开发环境
        if (options.keys) this.keys = options.keys;
        this.middleware = []; // 中间件
        this.context = Object.create(context);  // 创建上下文
        this.request = Object.create(request);  // 创建请求
        this.response = Object.create(response);// 创建响应
        if (util.inspect.custom) {
          this[util.inspect.custom] = this.inspect;
        }
    }
    
    • 看下koa和node的listen用法
      // node
      let app = http.createServer((req,res) => {
          res.end('123')
      })
      app.listen(3003, () => {
          console.log('success in 3003')
      })
      
      // koa
      app.listen(3000, () => {
        console.log('success in 3000')
      })
    
    • 看koa的listen用法,是在listen中传一个函数,这个函数包括node中的http.createServer和listen
    • 在看下koa的listen源码
      listen(...args) {
        debug('listen');
        const server = http.createServer(this.callback());
        return server.listen(...args);
      }
    
    • 从listen源码中看出果然是包括了node中的http.createServer和listen。
    • 在原生node中http.createServer中是一个包含req,res的回调函数,在koa中,被封装到了this.callback函数中,下面看koa的callback函数
      const compose = require('koa-compose');
      
      callback() {
        const fn = compose(this.middleware); // 使用compose集合中间件,下面再分析compose
    
        if (!this.listenerCount('error')) this.on('error', this.onerror);
    
        const handleRequest = (req, res) => {
          const ctx = this.createContext(req, res); // 将req, res传入createContext,返回ctx上下文
          return this.handleRequest(ctx, fn); // 将ctx(上下文),fn(中间件)传入handleRequest函数中
        };
    
        return handleRequest;
      }
    
    • koa-compse
      • 可以看到返回一个方法,里面其实是递归执行,从dispatch(0)开始执行,然后到return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));不断递归,假设已经middleware.length最后一个,那么就返回一个Promise.resolve();,也就是这段代码实现了洋葱圈模型,也就是app.use里面的next参数实际上是执行之后的middleware,这里有个官方 gif 图讲述这个过程。
      module.exports = compose
      function compose (middleware) {
          //传入的 middleware 参数必须是数组
        if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
        //middleware 数组的元素必须是函数
        for (const fn of middleware) {
          if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
        }
    
        /**
        * @param {Object} context
        * @return {Promise} 返回一个闭包函数,函数的返回是一个Promise 对象, 保持对 middleware 的引用。
        * @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]
            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)
            }
          }
        }
      }
    
    
    • 看下createContext做了什么
      • 看下面代码,主要是将req,res等挂载到ctx上下文中,并返回ctx。
      createContext(req, res) {
        const context = Object.create(this.context);
        const request = context.request = Object.create(this.request);
        const response = context.response = Object.create(this.response);
        context.app = request.app = response.app = this;
        context.req = request.req = response.req = req;
        context.res = request.res = response.res = res;
        request.ctx = response.ctx = context;
        request.response = response;
        response.request = request;
        context.originalUrl = request.originalUrl = req.url;
        context.state = {};
        return context;
      }
    
    • 看下handleRequest函数做了什么处理
      handleRequest(ctx, fnMiddleware) {
        // 从callback函数传过来的两个参数ctx,fnMiddleware
        const res = ctx.res; // 响应内容
        res.statusCode = 404; // 响应码
        const onerror = err => ctx.onerror(err);  //错误处理
        const handleResponse = () => respond(ctx);  // 正确的响应处理
        onFinished(res, onerror);
        return fnMiddleware(ctx).then(handleResponse).catch(onerror); // 将ctx传入中间件后处理正确的响应处理和错误处理
      }
    
    • koa通过use传入中间件,看下use函数
      • 看出只是将中间件push到middleware中,并返回this,支持链式调用。
      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;
      }