Koa2源码简读

koa2 源码非常精简,只有四个文件:

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

application.js

先从入口开始看,koa有几个关键的点:

  1. 导出的 koa 可以new
  2. use 方法
  3. listen 方法
  4. 错误抛出 error

koa可以new,说明koa.js返回是是一个类,可以被监听(on),说明也是一个events,所以返回的这个类是继承过event的。

const Emitter = require('events');

module.exports = class Application extends Emitter {
   constructor(options) {
    super();
    options = options || {};
    // 是否信任proxy header参数,默认为 false
    this.proxy = options.proxy || false;
    // 子域偏移量,默认为 2
    this.subdomainOffset = options.subdomainOffset || 2;
    // 代理ip标头,默认为 X-Forwarded-For
    this.proxyIpHeader = options.proxyIpHeader || 'X-Forwarded-For';
    // 从代理ip标头读取的最大ip数,默认为 0
    this.maxIpsCount = options.maxIpsCount || 0;
    this.env = options.env || process.env.NODE_ENV || 'development';
    if (options.keys) this.keys = options.keys;
    this.middleware = [];
    // Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);
    // util.inspect.custom support for node 6+
    /* istanbul ignore else */
    if (util.inspect.custom) {
      this[util.inspect.custom] = this.inspect;
    }
  }
};

需要注意,koa1.x 用的是构造函数方式,koa2.x 用的是类。所以调用的时候两者有差别。

const koa = require('koa');

// koa 1.x
const app = koa();

// koa 2.x
const app = new koa();

use

初始化koa实例化,我们需要调用app.use() 去挂载中间件。

use 做的事很简单,注册一个中间fn,将fn放入middleware数组中。

中间件可以在请求前(request) 和 响应后(response) 进行拦截修改context的request 和 response 。

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 || '-');
  // 将中间件函数放入middleware数组中
  this.middleware.push(fn);
  return this;
}

listen

listen方法,与node的listen如出一辙。 这里的http.createServer就是node原生启动http服务的方法。此方法接受两个参数:

  • options?: Object, 可选属性 [IncomingMessage, ServeResponse, insecureHTTPParser, maxHeaderSize]
  • requestListener?: Function, 接收req 和 res

this.callback方法会返回一个函数作为http.createSever的回调函数,然后进行监听。

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

callback

callback函数是koa的开始,中间件与request 和 response 调和的入口。

  1. compose, 这个方法会将之前use过的中间件全部组合起来,形成洋葱模型等待调用。
  2. createContext, 创建上下文,会接收原生nodejs http类提供的的req与res参数,生成ctx对象,使其贯穿整个koa
  3. handleRequest, 用来调和request、response和中间件。
callback() {
  // 用于生成koa洋葱模型
  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);
  };
  // node http.createServer 接收的就是这个函数
  return handleRequest;
}

compose

compose 方法主要负责生成洋葱模型,通过 koa-compose 包实现。

是一个高阶函数,会返回一个匿名函数。

module.exports = compose;

function compose (middleware) {
  // 传入的middleware 必须是一个数组
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  for (const fn of middleware) {
    // 传入的middleware每一个value必须是函数
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }

  return function (context, next) {
    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]
      // 处理最后一个中间件还有next的情况,没有next就直接resolve, 从下往上执行回去
      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)
      }
    }
  }
}

这段代码重点就是 fn(context, dispatch.bind(null, i + 1)), 递归。

dispatch.bind(null, i + 1) 就是我们通常写中间件的第二个参数next。

我们执行这个next() 方法实际上得到的是下一个中间件的执行。

var middleware = [];

function next() {
  return console.log('最外层的函数');
}

function createNext(middleware, next) {
  return function(){
    middleware(next);
  }
}

middleware.push(function(next){
  console.log(1);
  next();
  console.log(6);
})

middleware.push(function(next){
  console.log(2);
  next();
  console.log(5);
})

middleware.push(function(next){
  console.log(3);
  next();
  console.log(4);
})

var len = middleware.length;
for(var i = len-1;i>=0;i--) {
  var currentMiddleware = middleware[i];
  next = createNext(currentMiddleware, next)
}
next();

// 123456

createContext

生成koa的上下文,主要是用来引入request.js和response.js。

这里主要将req, res, this.request, this.response 挂载到context上。并通过赋值理清了循环引用层级关系,为使用者提供方便。

// const ctx = this.createContext(req, res);

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方法用来处理请求与响应。

handleRequest(ctx, fnMiddleware) {
  const res = ctx.res;
  // 默认设置响应状态码为 404
  res.statusCode = 404;
  const onerror = err => ctx.onerror(err);
  const handleResponse = () => respond(ctx);
  // 监听http response, 当请求结束时执行回调。传入的回调是 ctx.onerror(),即当发生错误时才执行
  onFinished(res, onerror);
  // 执行洋葱模型
  return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}

respond

用来处理响应,首先会判断 res 的 code是否为空,method 是否为 HEAD,body是否为空。以上条件都没命中,则会开始判断body的类型,是 Buffer, String , Stream 还是 JSON,做对应的返回。

function respond(ctx) {
  // 没有响应体直接返回
  if (false === ctx.respond) return;
  //  上下文不让写,返回
  if (!ctx.writable) return;

  const res = ctx.res;
  let body = ctx.body;
  const code = ctx.status;

  // ignore body
  if (statuses.empty[code]) {
    // strip headers
    ctx.body = null;
    return res.end();
  }

  if ('HEAD' === ctx.method) {
    if (!res.headersSent && !ctx.response.has('Content-Length')) {
      const { length } = ctx.response;
      if (Number.isInteger(length)) ctx.length = length;
    }
    return res.end();
  }

  // status body
  if (null == body) {
    if (ctx.response._explicitNullBody) {
      ctx.response.remove('Content-Type');
      ctx.response.remove('Transfer-Encoding');
      return res.end();
    }
    if (ctx.req.httpVersionMajor >= 2) {
      body = String(code);
    } else {
      body = ctx.message || String(code);
    }
    // 如果响应头没有发送,则去结算该文件的字节长度
    if (!res.headersSent) {
      ctx.type = 'text';
      ctx.length = Buffer.byteLength(body);
    }
    return res.end(body);
  }

  // responses
  if (Buffer.isBuffer(body)) return res.end(body);
  if ('string' === typeof body) return res.end(body);
  if (body instanceof Stream) return body.pipe(res);

  // body: json
  body = JSON.stringify(body);
  if (!res.headersSent) {
    ctx.length = Buffer.byteLength(body);
  }
  res.end(body);
}

onerror

需要区分清楚。这里有两个onerror,ctx.onerror 和 koa.onerror。

在handleRequest时处理错误的是ctx.onerror,在callback时绑定的错误处理是koa.onerror。

// context.js
onerror(err) {
  if (null == err) return;

  const isNativeError =
    Object.prototype.toString.call(err) === '[object Error]' ||
    err instanceof Error;
  if (!isNativeError) err = new Error(util.format('non-error thrown: %j', err));

  let headerSent = false;
  if (this.headerSent || !this.writable) {
    headerSent = err.headerSent = true;
  }

  this.app.emit('error', err, this);

  if (headerSent) {
    return;
  }

  const { res } = this;

  if (typeof res.getHeaderNames === 'function') {
    res.getHeaderNames().forEach(name => res.removeHeader(name));
  } else {
    res._headers = {}; // Node < 7.7
  }

  this.set(err.headers);

  // force text/plain
  this.type = 'text';

  let statusCode = err.status || err.statusCode;

  // ENOENT support
  if ('ENOENT' === err.code) statusCode = 404;

  // default to 500
  if ('number' !== typeof statusCode || !statuses[statusCode]) statusCode = 500;

  // respond
  const code = statuses[statusCode];
  const msg = err.expose ? err.message : code;
  this.status = err.status = statusCode;
  this.length = Buffer.byteLength(msg);
  res.end(msg);
}

// koa.js
onerror(err) {
  const isNativeError =
    Object.prototype.toString.call(err) === '[object Error]' ||
    err instanceof Error;
  if (!isNativeError) 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(`\n${msg.replace(/^/gm, '  ')}\n`);
}

content.js

content.js 主要的功能提供了对request和response对象的方法与属性便捷访问能力。 其中使用了node-delegates, 将context.request与context.response上的方法与属性代理到context上。

delegate(proto, 'response')
  .method('attachment')
  .method('redirect')
  .method('remove')
  .method('vary')
  .method('has')
  .method('set')
  .method('append')
  .method('flushHeaders')
  .access('status')
  .access('message')
  .access('body')
  .access('length')
  .access('type')
  .access('lastModified')
  .access('etag')
  .getter('headerSent')
  .getter('writable');


delegate(proto, 'request')
  .method('acceptsLanguages')
  .method('acceptsEncodings')
  .method('acceptsCharsets')
  .method('accepts')
  .method('get')
  .method('is')
  .access('querystring')
  .access('idempotent')
  .access('socket')
  .access('search')
  .access('method')
  .access('query')
  .access('path')
  .access('url')
  .access('accept')
  .getter('origin')
  .getter('href')
  .getter('subdomains')
  .getter('protocol')
  .getter('host')
  .getter('hostname')
  .getter('URL')
  .getter('header')
  .getter('headers')
  .getter('secure')
  .getter('stale')
  .getter('fresh')
  .getter('ips')
  .getter('ip');

request.js && response.js

request.js || response.js 封装了请求 || 响应 相关的属性以及方法。

通过application.js 中的 createContext 方法引入,挂载在koa ctx上。

最后

来梳理一下运行过程:

  1. koa在new Application 时, 会初始化 context, request, response, middleware 及其他属性。
    其中context, request, response 都有自己初始化属性,并且有单独的把代码抽出来放在对应文件中。

  2. 通过use对koa添加中间件,将中间件 push 到 中间件数组中。

  3. 最后通过 listen 方法,激活整个application,跟node原生启动服务一致。

  4. 由于http.createServer(this.callback()), 有个callback方法,作为http.createServer的回调函数。callback中有四条主线,分别是 compose, onerror, handleRequest, createContext

  5. compose, 将use使用的中间件进行拼接,形成一个洋葱模型,以await next() 上下分割,作为先后执行的顺序。

  6. onerror ,因为 Application 继承了 Events类,所以拥有监听事件的on方法。用于监听koa自身抛出的错误。

  7. callback函数里的handleRequest, 顾名思义是个请求处理器,主要的功能有两个。

  • 接收node原生http模块的req, res 对象,创建请求响应的上下文
  • 传入ctx与中间件,调和中间件、上下文。
  1. createContext, 用Object.create 重新初始化 context、request、req 三个对象,并将它们塞到原型链(__proto__)上。最后返回context,供外部使用。

  2. handleRequest 主要是处理中间件, 再处理then 的handleResponse,catch错误。

  3. 到这里koa源码大致就完成了 🎉

Last Updated:
Contributors: kk