目录

koa2 框架源码分析

koa2 是基于 Node.js 实现的一个 web 框架。非常简洁,轻量,所有的功能都以插件的形式实现,开发者可以 按需引入。

我们从一个官方示例开始,来看看 koa 的实现原理:

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

app.use(async ctx => {
  ctx.body = 'Hello World';
});

app.listen(3000);

koa 的源码主要有四个文件,分别是 application.jscontext.jsrequest.jsresponse.js

application.js 是 koa 的入口文件,app.useapp.listen 的实现就在这个文件中。

application.js

new Koa() 创建了一个 Application 实例,Application 的构造函数:

const Emitter = require('events');
class Application extends Emitter {
    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;
        }
    }
}

Application 类继承了 events,这样 app 就有了事件监听的能力。构造函数还为实例添加了一系列的属性,比如经常会用到的 middlewarecontextrequestresponse 等。

Application 还暴露了一些常用的方法,比如 listenuse 等等。

我们从 listen 方法开始分析:

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

listen 方法其实就是对 http.createServer 进行了一个简单的封装。这里启动的 http server。如果要启用 https,就不能使用 listen 方法了,可以直接使用 https 包来创建:

const https = require('https');
const Koa = require('koa');
const app = new Koa();
https.createServer(app.callback()).listen(3001);

listen 方法中应该关注 callback 的实现:

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

compose(this.middleware) 是引用的插件 koa-compose 的方法。 middlleware 是一个数组,存放的是通过 app.use 添加的中间件。

app.use 如何添加中间件:

use(fn) {
    // 检查传入的中间件是否是函数
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    if (isGeneratorFunction(fn)) { // 检查是否是 generator 函数,为了兼容 koa1
        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); // 将 koa1 中的 generator 函数转为 Promise 函数
    }
    debug('use %s', fn._name || fn.name || '-');
    this.middleware.push(fn); // 把中间件添加到 middleware 数组
    return this; // 返回 this,链式调用
}

compose 方法是 koa 中间件机制最重要的部分:

/**
 * Compose `middleware` returning
 * a fully valid middleware comprised
 * of all those which are passed.
 *
 * @param {Array} middleware
 * @return {Function}
 * @api public
 */

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) { // 这里返回一个函数,koa 的 ctx 和 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)
      }
    }
  }
}

compose 返回了一个函数, 先不管函数里面怎么执行,接着回到 Application 的callback 方法:

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

    if (!this.listenerCount('error')) this.on('error', this.onerror); //  listenerCount 是继承与 event 对象的方法。判断是否监听了 error 事件,
                                                                      // 如果没有,添加 error 事件监听

    const handleRequest = (req, res) => { // req 和 res 作为参数
      const ctx = this.createContext(req, res); // 使用原生 request 和 response 对象创建 koa 的 Context 对象
      return this.handleRequest(ctx, fn); // 处理请求,传入 compose 返回的 fn 函数,串行执行中间件
    };

    return handleRequest;
}

callback 方法返回了一个 handleRequest 函数,这是 http.createServer 接收的回调函数。handleRequest 方法被加入到 request 事件中。当服务器接收到 http 请求时,request 事件被触发,然后调用 handleRequest 方法。

handleRequest 方法又调用了 this.handleRequest(ctx, fn)

handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    res.statusCode = 404;
    const onerror = err => ctx.onerror(err); // koa 默认的错误处理回调函数,处理异常结束
    const handleResponse = () => respond(ctx); // 处理 response 的回调函数
    onFinished(res, onerror); // 监听 http response 的结束事件,执行 onerror 回调函数
    return fnMiddleware(ctx).then(handleResponse).catch(onerror); // 执行中间件,
}

fnMiddleware 就是这里 const fn = compose(this.middleware);fn,再看 fn 的实现:

function (context, next) {
    // last called middleware #
    let index = -1 // 防止 next 多次调用
    return dispatch(0) // 递归调用中间件
    function dispatch (i) {
        if (i <= index) return Promise.reject(new Error('next() called multiple times'))
        index = i // 从 0 开始,递归调用时加 1
        let fn = middleware[i]
        if (i === middleware.length) fn = next // 注意 fnMiddleware(ctx).then(handleResponse).catch(onerror) 调用时,
                                               // 没有传入 next,所以,这里执行完最后一个中间件后,fn 被赋值了 undefined
        if (!fn) return Promise.resolve() // 立即返回处于 Promise.resolve 状态实例,继续执行后面的逻辑
        try {
            // 用 Promise 包裹中间件,方便 await 调用
            // dispatch.bind(null, i + 1) 是下一个中间件,被当做 next 参数,传入了当前中间件
            // 这就是在中间件执行 next() 的时候就会进入下一个中间件的原理
            return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
        } catch (err) {
            return Promise.reject(err)
        }
    }
}

上面的代码就是 koa 中间件洋葱模型的实现。

/images/koa2-analysis/onion.png

洋葱模型是中间件的一种串行机制,并且是支持异步,第一个中间件中执行 next(),则会进入下一个中间件。

官方的中间件示例:

// logger
app.use(async (ctx, next) => {
  await next();
  const rt = ctx.response.get('X-Response-Time');
  console.log(`${ctx.method} ${ctx.url} - ${rt}`);
});

// x-response-time
app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  ctx.set('X-Response-Time', `${ms}ms`);
});

上面示例中 logger 中间件,调用了 await next(); 进入了 x-response-time 中间件中,next() (这里的 next 就是 x-response-time 中间件)执行完, 则继续执行下面的代码,获取 X-Response-Time 并打印日志。

context.js

Context 包含了两个部分:

  • 自身属性,框架内部使用
  • 通过 delegates 库,代理了 request, response 对象上的属性。

application.jscreateContext 方法创建 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;
}
/**
 * Response delegation.
 */

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');

/**
 * Request delegation.
 */

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');

上面的代码通过 delegate 代理了 ctx.requestctx.response 两个对象上的属性。 也就是说,你可以直接通过访问 ctx.status 来得到 ctx.repsponse.status 的值。

request.js、response.js

这两部分就是对原生的 http 模块 request、response 对象进行了封装,在对象属性上添加了 setter 和 getter。暴露了一些新的方法。

错误处理

koa 有两个 onerror 方法,一个是 Application 的,监听整个应用的 error 事件。一个是 Context 对象的 onerror,监听处理 http request 和 response 时的 error 事件。

application.jsonerror

onerror(err) {
    // 判断是否是 Error 类型
    if (!(err instanceof Error)) throw new TypeError(util.format('non-error thrown: %j', err));
    // 忽略 404 错误
    if (404 == err.status || err.expose) return;
    // 如果有静默设置, 则忽略
    if (this.silent) return;
    // 打印 error
    const msg = err.stack || err.toString();
    console.error();
    console.error(msg.replace(/^/gm, '  '));
    console.error();
}

application.jscallback 方法中有段代码:if (!this.listenerCount('error')) this.on('error', this.onerror);,如果开 发者没有调用 app.on('error', func)监听 error 事件,那么就会在这里添加默认的 onerror 回调来监听 error 事件。

context.jsonerror

onerror(err) {
    // don't do anything if there is no error.
    // this allows you to pass `this.onerror`
    // to node-style callbacks.
    if (null == err) return;
    // 将错误转化 Error 类型
    if (!(err instanceof Error)) err = new Error(util.format('non-error thrown: %j', err));

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

    // delegate
    // 触发 koa app 对象的 error 事件, application 上的 onerror 函数会执行
    this.app.emit('error', err, this);

    // nothing we can do here other
    // than delegate to the app-level
    // handler and log.
   // 如果响应头部已经发送(或者 socket 不可写), 退出函数
    if (headerSent) {
      return;
    }
    // 获取原生 http response 对象
    const { res } = this;

    // first unset all headers
    /* istanbul ignore else */
    if (typeof res.getHeaderNames === 'function') {
      res.getHeaderNames().forEach(name => res.removeHeader(name));
    } else {
      res._headers = {}; // Node < 7.7
    }

    // then set those specified
    this.set(err.headers);

    // force text/plain
    // 出错后响应类型为 text/plain
    this.type = 'text';

    // ENOENT support
    // 对 ENOENT 错误进行处理, ENOENT 的错误 message 是文件或者路径不存在, 所以状态码应该是 404
    if ('ENOENT' == err.code) err.status = 404;

    // default to 500
     // 默认状态码为 500
    if ('number' != typeof err.status || !statuses[err.status]) err.status = 500;

    // respond
    const code = statuses[err.status];
    const msg = err.expose ? err.message : code;
    // 设置响应状态码
    this.status = err.status;
     // 设置响应 body 长度
    this.length = Buffer.byteLength(msg);
    // 响应结束
    res.end(msg);
}

application.jshandleRequest 方法:

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

onFinish 函数中会调用 contextonerror 方法,来处理响应中的 error 事件。

koa-router

koa 本身并没有实现 router 的功能。需要引入插件。我们通过的 koa-router 的官方示例,来分析 一下路由是如何注册并执行的:

var Koa = require('koa');
var Router = require('koa-router');

var app = new Koa();
var router = new Router();

// use middleware only with given path
router.use('/users', userAuth());

// or with an array of paths
router.use(['/users', '/admin'], userAuth());

router.get('/', (ctx, next) => {
  // ctx.router available
});

app
  .use(router.routes())
  .use(router.allowedMethods());

koa-router 实现路由的核心文件是 router.jsrouter.js 也是入口文件。

Router 的构造函数:

function Router(opts) {
  if (!(this instanceof Router)) {
    return new Router(opts);
  }

  this.opts = opts || {};
  this.methods = this.opts.methods || [ // 路由方法
    'HEAD',
    'OPTIONS',
    'GET',
    'PUT',
    'PATCH',
    'POST',
    'DELETE'
  ];

  this.params = {};
  this.stack = []; // 存放注册的路由对象
};

router.js 中定义 router.get 或者 router.post 等方法:

// 遍历所有的 method
methods.forEach(function (method) {
 // 添加原型方法
  Router.prototype[method] = function (name, path, middleware) {
    var middleware;
    // 处理参数,第一个参数可以是路由 name,也可以是路由的 path
    if (typeof path === 'string' || path instanceof RegExp) {
      middleware = Array.prototype.slice.call(arguments, 2);
    } else {
      middleware = Array.prototype.slice.call(arguments, 1);
      path = name;
      name = null;
    }
    // 注册路由,这里的第二个参数是一个数组,是为了 all 方法注册时使用
    this.register(path, [method], middleware, {
      name: name
    });

    return this;
  };
});

所以调用 router.get 等方法(包括 router.allrouter.use)注册路由是其实是调用了 register 方法:

Router.prototype.register = function (path, methods, middleware, opts) {
  opts = opts || {};

  var router = this;
  var stack = this.stack;

  // support array of paths
  if (Array.isArray(path)) {
    // 如果 path 是一个数组,遍历所有 path,分别为每一个 path 注册路由
    path.forEach(function (p) {
      router.register.call(router, p, methods, middleware, opts);
    });

    return this;
  }

  // create route
  // 创建路由对象
  var route = new Layer(path, methods, middleware, {
    end: opts.end === false ? opts.end : true,
    name: opts.name,
    sensitive: opts.sensitive || this.opts.sensitive || false,
    strict: opts.strict || this.opts.strict || false,
    prefix: opts.prefix || this.opts.prefix || "",
    ignoreCaptures: opts.ignoreCaptures
  });

  // 添加路由前缀
  if (this.opts.prefix) {
    route.setPrefix(this.opts.prefix);
  }

  // add parameter middleware
  // // 设置 param 前置处理函数
  Object.keys(this.params).forEach(function (param) {
    route.param(param, this.params[param]);
  }, this);

  // 存储路由对象
  stack.push(route);

  return route;
};

注册完路由,必须通过 app.use(router.routes()) 方法将所有的路由,添加到 koa 的中间件,router.routes() 方法做了什么:

Router.prototype.routes = Router.prototype.middleware = function () {
  var router = this;
  // 有点似曾相识,类似 compose 的实现
  var dispatch = function dispatch(ctx, next) {
    debug('%s %s', ctx.method, ctx.path);

    var path = router.opts.routerPath || ctx.routerPath || ctx.path;
    // 匹配路由
    var matched = router.match(path, ctx.method);
    var layerChain, layer, i;
    // 将匹配的路由缓存到 context 对象
    if (ctx.matched) {
      ctx.matched.push.apply(ctx.matched, matched.path);
    } else {
      ctx.matched = matched.path;
    }

    ctx.router = router;

    if (!matched.route) return next(); // // 未匹配到路由,执行下一个中间件

    var matchedLayers = matched.pathAndMethod
    var mostSpecificLayer = matchedLayers[matchedLayers.length - 1]
    ctx._matchedRoute = mostSpecificLayer.path;
    if (mostSpecificLayer.name) {
      ctx._matchedRouteName = mostSpecificLayer.name;
    }
    // 路由的前置处理中间件 将 params、路由别名以及捕获数组属性挂载到 context 上下文对象中
    layerChain = matchedLayers.reduce(function(memo, layer) {
      // 将所有的 layer 封装成了 koa 的中间件函数
      memo.push(function(ctx, next) {
        ctx.captures = layer.captures(path, ctx.captures);
        ctx.params = layer.params(path, ctx.captures, ctx.params);
        ctx.routerName = layer.name;
        // 进入下一个路由中间件
        return next();
      });
      return memo.concat(layer.stack);
    }, []);

    // 返回了 compose 函数,这个函数也同样式依赖 `koa-compose`
    // 将所有匹配的路由 和 路由中间件的数组传入,并执行 compose 返回的函数
    // 注意 koa 是在 `application.js` 的 `callback` 方法中执行 compose 返回的函数
    // 这里利用 compose 函数,又实现了一个洋葱模型
    return compose(layerChain)(ctx, next);
  };

  dispatch.router = this;

  return dispatch;
};

routes 方法返回了 dispatch 函数。dispatch 函数被注册到了 koa 的中间件,那么按照 koa 中间件的执行机制,dispatch 函数 最终会在某个 koa 中间件中执行 next 时被执行。

router.match 的实现:

Router.prototype.match = function (path, method) {
  var layers = this.stack;
  var layer;
  var matched = {
    path: [],
    pathAndMethod: [],
    route: false
  };
  // 遍历所有存放的路由数组
  for (var len = layers.length, i = 0; i < len; i++) {
    layer = layers[i];

    debug('test %s %s', layer.path, layer.regexp);
    // 调用路由对象 layer 的 math 函数,就是一个正则匹配
    // 将匹配到的 layer 放到 matched.path 数组
    if (layer.match(path)) {
      matched.path.push(layer);
      // layer 的 methods 数组存放的是注册的路由方法
      // 如果 layer.methods.length === 0 该 layer 为路由级别的中间件,即 route.use 方法注册的路由函数
      // ~layer.methods.indexOf(method) -1 按位取反是 00000000,所以这个是判断路由方法被匹配到
      if (layer.methods.length === 0 || ~layer.methods.indexOf(method)) {
        matched.pathAndMethod.push(layer);
       // 当路由的路径和路由方法都被满足时,才算是路由被匹配到,将 matched.route 置为 true
        if (layer.methods.length) matched.route = true;
      }
    }
  }

  return matched;
};
function Layer(path, methods, middleware, opts) {
  this.opts = opts || {};
  this.name = this.opts.name || null;
  this.methods = [];
  this.paramNames = [];
  this.stack = Array.isArray(middleware) ? middleware : [middleware];

  methods.forEach(function(method) {
    var l = this.methods.push(method.toUpperCase());
    if (this.methods[l-1] === 'GET') {
      this.methods.unshift('HEAD');
    }
  }, this);

  // ensure middleware is a function
  this.stack.forEach(function(fn) {
    var type = (typeof fn);
    if (type !== 'function') {
      throw new Error(
        methods.toString() + " `" + (this.opts.name || path) +"`: `middleware` "
        + "must be a function, not `" + type + "`"
      );
    }
  }, this);

  this.path = path;
  this.regexp = pathToRegExp(path, this.paramNames, this.opts);

  debug('defined route %s %s', this.methods, this.opts.prefix + this.path);
};