koa源码分析

koa源码非常简单,只有四个文件也就是koa的四大对象:

  • application.js 包含 app 的构造以及启动一个服务器
  • context.js app 的 context 对象, 传入中间件的上下文对象
  • request.js app 的请求对象,包含请求相关的一些属性
  • response.js app 的响应对象,包含响应相关的一些属性

application主要方法

application中的构造函数:

constructor() {
  super();

  this.proxy = false;
  // 存放中间件的数组
  this.middleware = [];
  // 忽略的子域名数量
  this.subdomainOffset = 2;
  // 设置环境变量
  this.env = process.env.NODE_ENV || 'development';
  // 挂载context,request,response到application
  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;
  }
}

application除了构造方法外还有几个主要的方法,包括:

applicaton的createContext方法:

  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;
  }

创建了一个context上下文对象,这个对象挂载了app,req,res,ctx等多个属性,属性的含义看缩写即可明白。

application中的listen方法

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

调用了基础网络库http中的createServer和linsten方法,创建一个服务器并且监听端口。

koa中间件源码原理

application中的use方法:

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;
}

因为中间件是一个用于拦截请求的异步函数,所以首先判断中间件类型是否正确,并且在中间件是generator函数的情况下转为异步函数,然后把推入中间件数组并且返回本身。该步骤仅仅作为注册中间件的功能,中间件数组中的中间件将会在http发生时被依次调用。调用的过程如下:

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

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

  const handleRequest = (req, res) => {
    const ctx = this.createContext(req, res);
    return this.handleRequest(ctx, fn);
  };

  return handleRequest;
}

这边中间件的执行用了一个koa-compose的库,koa-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
   * @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]
      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)
      }
    }
  }
}

前几行做类型判断,不再赘述。
主要内容是返回一个函数,这个函数是一个递归的高阶函数。
最关键的一行是return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
这就会在promise resolve的时候执行fn(context, dispatch.bind(null, i + 1))
这个过程可以被理解为

function compose(){
  return Promise.resolve(
    f1(context,function(){
      return Promise.resolve(
        f2(context,function(){
          return Promise.resolve(
            f3(context,function(){
              return Promise.resolve(
                next(context,function(){
                  return Promise.resolve('fn is undefined')
                })
              )
            })
          )
        })
      )
    })
  )
}

在一个中间件函数中不能调用两次next(),否则会抛出错误。
为什么执行顺序是1,2,3,4,5,ok,5,4,3,2,1呢。是因为 next() 是把主线程暂时交给下个代码块,所有代码块执行完后会依次收回执行权,而收回的顺序就相反了。

context主要方法

相比application,剩下三部分都要简单的多。
Koa处理请求的过程:当请求到来的时候,会通过req和res来创建一个context (ctx),然后执行中间件,然后再返回响应。
context.js是用来代理ctx的功能,把更多的方法和功能挂载上去,提供对request和response的更多操作。其中最主要的方法是delegate方法,除此之外的方法都是辅助方法或者错误捕获。

delegate(proto, 'response')
  .method('attachment')
  // ...
  .getter('writable');

delegate(proto, 'request')
  .method('acceptsLanguages')
  // ...
  .getter('ip');

这段操作使得,当访问proto的代理属性的时候,实际上是在访问proto.response的对应属性。

#request和response
两者类似,虽然代码比较长,但实际就是对HTTP的header的一个处理而已,每个方法都非常简单,也调用了一些基础的网络库如url,net等等。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

😉😐😡😈🙂😯🙁🙄😛😳😮:mrgreen:😆💡😀👿😥😎😕