阅读 294

Koa源码解析

概述

Koa和Express框架都是出自同一团队的,我们常常拿它们作比较。

Koa比Express更加轻便,Koa没有集成路由功能、模板引擎,仅保留了中间件功能。

而且通过阅读源码,你会发现Koa比Express更易读,Koa使用了比较新的ES 6的语法,有class、extend关键字,也有promise异步风格等等。

整体思路

把Koa app分为两个阶段:

  1. 服务器准备阶段,这个阶段主要是注册中间件和整合中间件

  2. 服务器响应请求阶段,这个阶段主要是代理req对象和res对象,遍历并执行中间件函数

服务器准备阶段

这一阶段会进行初始化Koa实例的操作,比如装载中间件的middleware数组,增强了的req对象和res对象,以及新增的上下文对象context,可以从下面的构造函数看出来:

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;
    }
}
复制代码

1. 注册中间件

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;
}
复制代码

2. 整合中间件

这里依赖的是koa-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)
      }
    }
  }
}
复制代码

从中可以看出,整合中间件的原理是,通过闭包维护中间件数组,然后返回一个接收context上下文对象的函数,该context会被传入所有中间件。

或许你也注意到了,所有中间件执行的结果都被传入Promise.resolve,所以我们的中间件函数是可以返回普通对象,也可以返回Promise实例的。这也决定了,使用Koa的用户可以使用最新的ES 2017的语法糖:async / await(当然也可以通过babel转译)。

在中间件函数被执行的时候,不仅传入了context对象,还传入了下一个中间件的引用,所以中间件可以暂时把执行控制权交给下一个中间件,这一点跟Express是一样的。不同的地方在于,Express的中间件不能处理异步操作,而Koa可以,因为Koa的中间件可以返回一个Promise实例。

服务器响应请求阶段

跟普通的NodeJS服务器一样,当服务器接收到客户端请求,也会触发request事件,这时Koa会把request handler传入的req和res整合并加强成上下文对象context,然后遍历所有的中间件函数并传入context对象。

1. 整合并加强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;
}
复制代码

2. 遍历中间件

先看下面的代码:

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);
}
复制代码

传入的fnMiddleware函数其实是在准备阶段整合过的中间件,它会安装顺序执行或者通过next把执行权提前交给下一个中间件,直到执行玩所有中间件,最后返回一个Promise实例。

从中我们可以看到fnMiddleware返回Promise实例后,会执行handleResponse,它的作用就是把经过所有中间件处理过的res对象最后输出给客户端,它的主要源码如下:

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);
}
复制代码

看到这里,你应该明白为什么官方文档有一段话是这样的:

绕过 Koa 的 response 处理是 不被支持的. 应避免使用以下 node 属性:

  • res.statusCode
  • res.writeHead()
  • res.write()
  • res.end()

因为res的最后处理并返回给客户端是Koa框架来完成的,这一点跟Express框架不一样,在Express框架里,在中间件函数里调用res.end()后,排在后面的中间件的对res对象的处理将无效。

推荐的插件

因为Koa框架自身很轻,所以我们可以自主选择一些插件来增强它,下面就是推荐的一些插件:

  1. koa-router
  2. koa-views
  3. koa-bodyparser
  4. koa-static

参考

Koa@2.5.3 Koa中文文档

关注下面的标签,发现更多相似文章
评论