高质量 - Koa 源码解析

Koa 源码解析

一个简单的 koa 程序

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

// response
app.use(ctx => {
  ctx.body = 'Hello Koa';
});

app.listen(3000);

源码解析- koa 源码目录

  • application.js: koa 的导出文件夹,也就是定义 koa 的地方
  • context.js: 上下文,也就是常见的 ctx。每个 app 都会有一份 context,经过继承这个 context.js 中定义的 context。同时每个请求中,都会有一份单独的 ctx, 经过继承 app 中定义的 context。说白了就是复用。
  • request.js: 同前面的 context.js 说明。
  • response.js: 同前面的 context.js 说明。

说明:在 koanodejs 原生对应的是 reqresnode

源码解析- application.js


包的功能能够参考截图中给出的注释json

Application


这个就是 koa 的定义的类api

第一步 - new koa()

New 会执行构造函数。

因此在实例化的时候,能够传入这些 options数组

第二步 - app.use


在这里会检查 middleware 的类型,若是是老的 middleware 会转换一下,最后直接放到 middleware 这个数组中。数组中的中间件,会在每个请求中去挨个执行一遍。cookie

第三步 - app.listen

listen 的时候,才会去建立 server

对于每个请求,都会走到 callback 中去,因此 callback 是用于处理实际请求的。通常不要去重写这个 callbackapp

接下来去看看 callback 作了什么:

这里涉及到几个大的点:dom

  1. createContext 都干了什么
  2. Compose 是如何实现洋葱模型的。
  3. this.handleRequest(ctx, fn) 干了什么

这几个点分红两个大块来说,二、3 两点放到一块儿讲。koa

createContext 干了什么


这里作了三件重要的事情socket

  1. 每个 app 都有其对应的 context、request、response 实例,每个请求,都会基于这些实例去建立本身的实例。在这里就是建立了 context、request、response
  2. node 原生的 res、req 以及 this 挂载到 context、request、response 上面。还有一些其余为了方便访问作得一些挂载,不过前面三个的挂载是必须的。
  3. 将建立的 context 返回,传给全部中间件的第一个 ctx 参数,做为这个请求的上下文

下面着重解释一下第二点中,为何要把这些属性挂载上去。由于全部的访问都是代理,最终都是访问的 req、res 上面的东西,context 访问的是 request、response 上面的东西,可是他们上面的东西又是访问的是 req、res 上面的。

例如访问 ctx.methodcontext 会去 request 上面早,而后 request 会返回 req.method。后面分析其余文件时会讲到这种代理结构。ide

compose 如何实现的洋葱模型


在第三步中最后讲到的 callback 中,middleware 所有经过 koa-compose 这个包包装,返回了一个可执行的方法,在请求阶段会去执行这个方法,从而执行每个中间件。先本身来手撸一个 compose 的🌰

function compose(middleware) {
    return function (ctx, next) {
        function dispatch(i) {
            if (i >= middleware.length) {
                return Promise.resolve()
            }
            let curMiddleware = middleware[i]
            return curMiddleware(ctx, dispatch.bind(null, i + 1))
        }
        return dispatch(0)
    }
}

function mid1(ctx, next) {
    console.log('mid1 before')
    next()
    console.log('mid1 after')
}
function mid2(ctx, next) {
    console.log('mid2 before')
    next()
    console.log('mid2 after')
}
function mid3(ctx, next) {
    console.log('mid3 before')
    next()
    console.log('mid3 after')
}

const fn = compose([mid1, mid2, mid3])
fn({})
--------------------------------------------------------------------打印结果
mid1 before
mid2 before
mid3 before
mid3 after
mid2 after
mid1 after

compose 中会根据 i 去挨个执行中间件,而且有一个回溯的过程。官方代码以下。

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!')
  }

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

因此总结一下,洋葱模型本质是经过递归去实现的。

讲了 compose 的原理以后,回到第三步中最后的 this.handleRequest(ctx, fn); fn 就是compose 返回的包装过 middleware 的函数。下面进入 handleRequest

能够看到当一个请求来到的时候,最后会去执行包装过的中间件函数,也就是这里的最后一行,并在中间件执行完毕以后,到 handleResponse 中去处理响应。在 handleResponse 中最终执行的是 respond

respond

function respond(ctx) {
  // allow bypassing koa
  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.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);
}

主要是将 ctx 上挂载的 body 经过 res.end 返回响应。

源码解析 - request.js

module.exports = {
   /**
   * Return request header.
   *
   * @return {Object}
   * @api public
   */
  get header() {
    return this.req.headers;
  },

  /**
   * Set request header.
   *
   * @api public
   */

  set header(val) {
    this.req.headers = val;
  },
 ..............
}

可见前面在 createContext 的时候在 request 上面去挂载 req、res 的缘由就在这里。

源码解析 - response.js

module.exports = {
    /**
   * Return the request socket.
   *
   * @return {Connection}
   * @api public
   */

  get socket() {
    return this.res.socket;
  },
    /**
   * Get response status code.
   *
   * @return {Number}
   * @api public
   */
  get status() {
    return this.res.statusCode;
  },
.......................
}

源码解析 - context.js

const proto = module.exports = {
.............
  get cookies() {
    if (!this[COOKIES]) {
      this[COOKIES] = new Cookies(this.req, this.res, {
        keys: this.app.keys,
        secure: this.request.secure
      });
    }
    return this[COOKIES];
  },

  set cookies(_cookies) {
    this[COOKIES] = _cookies;
  }
............
}

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

这里的 proto 就是 context,在自身定义了一个经常使用的方法,可经过 ctx.method 去访问,还有后面使用 delegate ,这个函数会把自 context 上面的 request、response 上面的一些属性定义到 proto 也就是 context 上面去,可是当使用 ctx.xxx 去访问的时候,实际上是访问 request、response 上面的属性,这也是为何须要将 request、response 挂载到 context 上面去。

相关文章
相关标签/搜索