阅读学习Koa Core源码,基于koa v2.5.2。

Koa的核心代码很少,就四个文件application, context, request, response,算上注释和空行目前也还没过2000行代码。

这一篇针对application的源码进行阅读学习。

# Koa Core

koajs/koa

  • lib/application
  • lib/context
  • lib/request
  • lib/response

主要这四个文件,当然也还依赖了很多外部库,以及koa的其他仓库。这一篇看第一部分lib/application

# Application

Koa 的 Hello, world 是这样:

const Koa = require('koa')
const app = new Koa()
app.use(ctx => {
  ctx.body = 'Hello Koa'
})
app.listen(3000)

第一个const Koa = require('koa')就是引入koa的Application 类,即源码中的 lib/application.js

// package.json
{
  // ...
  "main": "lib/application.js",
  // ...
}

# constructor()

// @see https://nodejs.org/dist/latest-v10.x/docs/api/events.html#events_class_eventemitter
const Emitter = require('events');
module.exports = class Application extends Emitter {
  // ...
}

整个Application类继承自node的EventEmitter。也就是说,Application本身会带有on(), off(), emit()等事件相关的方法。

const context = require('./context');
const request = require('./request');
const response = require('./response');
const util = require('util');
// ...
/**
 * Initialize a new `Application`.
 *
 * @api public
 */
constructor() {
  super();
  this.proxy = false;
  this.middleware = [];
  this.subdomainOffset = 2;
  this.env = process.env.NODE_ENV || 'development';
  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的属性。

其中的context, request, response就是在另外三个文件中实现的了。

node v6.6.0+ 增加了自定义inspect函数,deprecate了原本的inspect()方法。为了支持旧版本node,还保留了this.inspectutil.inspect.custom将返回一个Symbol,专门用于对应inspect方法。预计不再支持旧版node后,会把这里移除,直接用[util.inspect.custom]


# use()

// @see https://github.com/visionmedia/debug
const debug = require('debug')('koa:application');
const isGeneratorFunction = require('is-generator-function');
const deprecate = require('depd')('koa');
const convert = require('koa-convert');
// ...
/**
 * Use the given middleware `fn`.
 *
 * Old-style middleware will be converted.
 *
 * @param {Function} fn
 * @return {Application} self
 * @api public
 */
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;
}
// ...

koa默认支持通过debug进行调试,只需要启动时增加环境变量DEBUG=koa*,即可调试koa相关的组件代码 - 参考文档

这里注册了koa:application为模块名,所以在当前文件中的debug都会属于该模块。后面出现debug的地方不再特别说明。

简单说来,use()方法就是往this.middileware数组中push新的middleware。由于koa的middleware洋葱模型是有顺序的,所以this.middileware数组中的顺序就是从外到内的顺序。

旧版1.xmiddleware还不是async函数,而是generator函数。为了支持旧版middleware,对generator函数进行了判断,通过koa-convert转换成async版的middleware,并通过deprecate提示旧版的middleware已经被废弃。预计3.x就不再支持旧版middleware了。

use()支持链式调用,所以最后返回的是this


# listen()

const http = require('http');
// ...
/**
 * Shorthand for:
 *
 *    http.createServer(app.callback()).listen(...)
 *
 * @param {Mixed} ...
 * @return {Server}
 * @api public
 */
listen(...args) {
  debug('listen');
  const server = http.createServer(this.callback());
  return server.listen(...args);
}
// ...

listen()方法就是调用了http模块并创建了http.Server实例进行listen

this.callback()作为requestHandler传入,监听request事件 - 文档


# callback()

const compose = require('koa-compose');
// ...
/**
 * Return a request handler callback
 * for node's native http server.
 *
 * @return {Function}
 * @api public
 */
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;
}
// ...

callback()就是传入http模块的requestHandler,通过koa-compose将所有通过use()注册的middlewares打包成一个,然后通过handleRequest()去真正处理请求。

listenerCount()是node的EventEmitter的方法。如果还没有给Application单独注册过error事件的监听器,则默认使用onerror来处理error事件。

通过this.createContext()方法新建上下文,作为后续整个middleware链中的ctx。因为每个请求都要有独立的context,所以每次处理请求时都要新创建一个。


# handleRequest()

const onFinished = require('on-finished');
// ...
/**
 * Handle request in callback.
 *
 * @api private
 */
handleRequest(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);
}
// ...

执行middlerware就是在这里了,将该请求的ctx传入之前通过koa-compose打包好的fnMiddleware中,执行完成后通过respond生成最终的响应返回给客户端。

如果res.statusCode没有被改过,说明没进入middleware,资源未找到,并且也没有报错,所以默认是设置了404。后面如果设置了body就会改成200,如果有别的错误码就会改成别的。

onFinished()的话,简单去看了看on-finished源码,就是在HTTP请求/响应关闭(closes)、完成(finishes)或者出错(errors)时,执行一个回调函数。那么这里就是在响应res出错的时候,执行context.onerror()方法。应该是因为响应出错的时候,有可能不会被后面的catch捕捉到,所以这里才额外写了一个方法。

fnMiddleware(ctx)传入ctx,将所有注册的middleware执行一遍,最后最处理完成的ctx执行respond方法,生成响应返回给客户端。


# respond()

其实这个respond()方法并不是Application类的方法,是写在类外的一个helper函数。

const isJSON = require('koa-is-json');
const Stream = require('stream');
// ...
/**
 * Response helper.
 */
function respond(ctx) {
  // allow bypassing koa
  if (false === ctx.respond) return;
  const res = ctx.res;
  if (!ctx.writable) return;
  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 && isJSON(body)) {
      ctx.length = Buffer.byteLength(JSON.stringify(body));
    }
    return res.end();
  }
  // status body
  if (null == body) {
    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);
}

如果设置了ctx.respond = false,则跳过koa默认的响应方法。如果!ctx.writable(即对应到res.socket.writable,或者res.isfinished),也跳过默认响应方法。

后面则是对响应的body进行判断和处理,如果不是string, Buffer, Stream等类型,则默认JSON.stringify处理后返回。


# createContext()

/**
 * Initialize a new context.
 *
 * @api private
 */
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,把ctx, res, req互相挂在一起。

TIP

其实这里有点不太明白,在构造函数里面:

this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);

已经是继承了一层了,在这里为什么还要再多套一层,又跑一次Object.create()

直接在createContext的时候这么写会有什么问题吗:

createContext(req, res) {
  const context = Object.create(context);
  const request = context.request = Object.create(request);
  const response = context.response = Object.create(response);
  // ...
}

# onerror()

// ...
/**
 * Default error handler.
 *
 * @param {Error} err
 * @api private
 */
onerror(err) {
  if (!(err instanceof Error)) 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();
  console.error(msg.replace(/^/gm, '  '));
  console.error();
}
// ...

默认的错误处理方法,注意是在callback()里面this.on('error')注册的,是对Application上的error进行处理的方法,不是响应的error

handleRequest()里面对应的响应的onerror是在context下实现的。


# toJSON(), inspect()

const only = require('only');
// ...
/**
 * Return JSON representation.
 * We only bother showing settings.
 *
 * @return {Object}
 * @api public
 */
toJSON() {
  return only(this, [
    'subdomainOffset',
    'proxy',
    'env'
  ]);
}
/**
 * Inspect implementation.
 *
 * @return {Object}
 * @api public
 */
inspect() {
  return this.toJSON();
}
// ...

打印app时的方法。

only就是通过白名单的方式,过滤掉对象中的多余属性,返回一个只包含相应属性的新对象。

# References

Koa源码阅读:
Koa Core - 源码阅读 2 - Context
Koa Core - 源码阅读 3 - Request & Response