Koa 源码阅读笔记

614 阅读7分钟

这篇文章介绍一个应用服务器框架的主要两个过程: app init 过程和 request handle 过程. 一些有趣的细节问题看看以后再写, 包括 context, request, response 三个对象, 错误处理, egg.js 等等.

这是我读的第二个框架, 它比 Flask 简单多了...

init 过程

通过一个简单的 demo (实际上就是官网的例子) 来讲解 app init 过程. 对于 Koa 来说, init 过程是比较简单的.

const Koa = require('koa')
const app = new Koa() // Koa 对象实例化

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

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

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

app.listen(3000) // 监听端口

Koa 对象实例化

lib/application.js

  constructor() {
    super();

    this.proxy = false;
    this.middleware = [];
    this.subdomainOffset = 2;
    this.env = process.env.NODE_ENV || 'development';

    // 在应用程序实例上绑定 context request repsonse 的原型, 实际上这三个对象都没有任何属性
    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;
    }
  }

use 增加 middleware

  use(fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');

    // 如果是一个生成器函数要转换成 async 函数, 细节问题
    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 当中
    this.middleware.push(fn);
    return this; // 通过返回自己可以进行链式调用
  }

监听端口

  listen(...args) {
    debug('listen');
    const server = http.createServer(this.callback());
    return server.listen(...args); // 调用 Node.js 原生的方法监听端口
  }

this.callback() 方法返回一个回调函数, 它符合 Node.js 原生 http.createServer 的要求, 被当作 request handler.

  callback() {
    // 将自己绑定的中间件封装起来
    const fn = compose(this.middleware);

    // 进行错误处理的回调函数
    // 由于 Koa 继承了 Emitter, 所以用户可以在上面绑定 error 方法, 如果用户没用绑定, 就绑定自带的 onerror 方法
    // 错误处理暂时不讲
    if (!this.listenerCount('error')) this.on('error', this.onerror);

    // http server 的回调函数
    const handleRequest = (req, res) => {
      // 将 request response 对象封装为 context 对象, 然后开始对 request 的处理过程, 这个放到第二节再讲
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn); // 返回对 request 的处理结果
    };

    return handleRequest;
  }

compose

这是个很重要的方法, 其返回的 fn, 将会在请求到达的时候实际负责 context 在 middleware 中的传递.

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
   */

  // 这个函数签名就是 koa middleware 常见的函数签名
  // 它作为 this.handleRequest 的参数, this.handleRequest 会调用它
  // 注意! 下面的代码及注释请在阅读 request handler 的过程阅读
  return function (context, next) {
    // last called middleware #
    // 指示 context 在 middleware 链上的位置
    // context 刚来的时候没有进入链, 所以 index === -1
    let index = -1

    // 从第 1 个 middleware 开始 context 之旅, index === 0
    return dispatch(0)

    // 这个 dispatch 串接 context 在 middleware 中的流动
    function dispatch (i) {
      // 如果 i 到了起点之前, 说明 next 被用了太多次
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i // 压栈阶段, 记录自己所在的位置
      let fn = middleware[i]
      if (i === middleware.length) fn = next // 如果走到了 middleware 的最后一站, 那么就用传入的 next 当作 next
      if (!fn) return Promise.resolve() // 如果都没有 middleware, 直接返回, 然后层层 resolve 返回
      try {
        // 进入 middleware 函数的执行过程, middleware 中访问的 next 被定义在这里, 
        // 而当这个 middleware 调用 next 的时候, 就等于调用 dispatch, 同时进入 middleware 的下一层
        // 如果当前 middleware 是最后一个, 上面的 if (i === middleware.length) fn = next 逻辑就会被激活, 顶层调用 next
        // 可以看到我们 await 的东西就是一个 resolved 的 Promise!
        // 根据 async 函数的定义, 默认返回的就是一个 resolved 的 Promise<undefined>
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        // 如果抛出了异常, 就会被捕获, 层层 reject 回来
        return Promise.reject(err)
      }
    }
  }
}

request handle 过程

还是用上面的例子来讲解 request handle 过程.

当有 http 请求过来的时候, 如下的方法最先被调用:

  const handleRequest = (req, res) => {
    const ctx = this.createContext(req, res); // 创建 context
    return this.handleRequest(ctx, fn); // 过程处理 === context 在 middleware 中的传递
  };

创建 context

  createContext(req, res) {
    // 创建三个对象, 将它们的 prototype 分别指向 this.context, this.request, this.repsonse, 实际上这三个对象 hasOwnProperties 为空
    // 然后就是各种引用, 比较简单
    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;
  }

处理过程

对 request 的实际处理过程.

  handleRequest(ctx, fnMiddleware) {
    // 这里的 fnMiddleware 即是 compose() 返回的 fn 的函数, 可以看到并没有给第二参数传递值, 所以在那里 next === undefined, 直接 resolve
    const res = ctx.res;
    res.statusCode = 404;

    // 准备两个 Promise 的回调函数
    const onerror = err => ctx.onerror(err);
    const handleResponse = () => respond(ctx);
    onFinished(res, onerror);
    
    // 开始 middleware 的传递过程
    // 如果执行过程成功, 并没有从 Promise 里拿任何的参数, 是利用闭包访问的 ctx 来生成响应的
    // 但执行失败则要从 Promise 链条里拿到错误信息
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }

context 在 middleware 中间的传递

fnMiddleware 被调用的时候, 即这个函数被调用:

function (context, next) {
  // last called middleware #
  // 指示 context 在 middleware 链上的位置
  // context 刚来的时候没有进入链, 所以 index === -1
  let index = -1

  // 从第 1 个 middleware 开始 context 之旅, index === 0
  return dispatch(0)

  // 这个 dispatch 串接 context 在 middleware 中的流动
  // 注意! 下面的代码及注释在阅读 request handler 的过程阅读
  function dispatch (i) {
    // 如果 i 到了起点之前, 说明 next 被用了太多次
    if (i <= index) return Promise.reject(new Error('next() called multiple times'))
    index = i // 压栈阶段, 记录自己所在的位置
    let fn = middleware[i]
    if (i === middleware.length) fn = next // 如果走到了 middleware 的最后一站, 那么就用传入的 next 当作 next
    if (!fn) return Promise.resolve() // 如果都没有 middleware, 直接返回, 然后层层 resolve 返回
    try {
      // 进入 middleware 函数的执行过程, middleware 中访问的 next 被定义在这里, 
      // 而当这个 middleware 调用 next 的时候, 就等于调用 dispatch, 同时进入 middleware 的下一层
      // 如果当前 middleware 是最后一个, 上面的 if (i === middleware.length) fn = next 逻辑就会被激活, 顶层调用 next
      // 可以看到我们 await 的东西就是一个 resolved 的 Promise!
      // 根据 async 函数的定义, 默认返回的就是一个 resolved 的 Promise<undefined>
      return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
    } catch (err) {
      // 如果抛出了异常, 就会被捕获, 层层 reject 回来
      return Promise.reject(err)
    }
  }
}

可以看到这个函数是递归的:

用我们的例子:

  1. 执行 dispatch(0), 我们注册的第一个异步函数被当成 fn, 然后 fn(context, dispatch.bind(null, 1)) 调用了这第一个异步函数
  2. 第一个异步函数执行 next(), 实际上执行了 dispatch(1), 然后调用了第二个异步函数...
  3. 同理, 调用了第三个异步函数, middleware 到这里已经全部执行过了
  4. 第三个函数执行的时候没用再调用 next(), 所以异步函数返回了状态为 resolved 的 Promise<undefined>
  5. return Promise.resolve() 把异步函数返回的 Promise 接着 resolved 下去
  6. 直到 dispatch(0) 中的 resolved 的 Promise 被 return 出去
  7. handleRequest 进入 fnMiddleware(ctx).then(handleResponse), 执行 handleResponse
例外情形
如果最后一个中间件也调用了 next

此时 fn === undefined, 并且 next === undefined, 所以就会直接返回已 resolved 的 Promise, 开始回溯.

如果有一个中间件调用了两次 next

我们已经知道每次调用 next 实际是调用了一次 dispatch(i), 如果我们调用了同一个 next 两次, 那么第二次调用的时候, i === index 的条件就会成立. 我们说过 index 是指示 context 在 middleware 中的位置的.

创建响应

function respond(ctx) {
  // allow bypassing koa
  // 允许 bypass 直通 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 的各种可能情况, 然后调用 http 模块 res 的方法返回响应.