Koa 源码阅读笔记

栏目: Node.js · 发布时间: 6年前

内容简介:这篇文章介绍一个应用服务器框架的主要两个过程: app init 过程和 request handle 过程. 一些有趣的细节问题看看以后再写, 包括 context, request, response 三个对象, 错误处理, egg.js 等等.这是我读的第二个框架, 它比 Flask 简单多了...通过一个简单的 demo (实际上就是官网的例子) 来讲解 app init 过程. 对于 Koa 来说, init 过程是比较简单的.

这篇文章介绍一个应用服务器框架的主要两个过程: 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 的方法返回响应.


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

智能革命

智能革命

李彦宏 等 / 中信出版集团 / 2017-5-1 / 68.00元

人类历史上的历次技术革命,都带来了人类感知和认知能力的不断提升,从而使人类知道更多,做到更多,体验更多。以此为标准,李彦宏在本书中将人工智能定义为堪比任何一次技术革命的伟大变革,并且明确提出,在技术与人的关系上,智能革命不同于前几次技术革命,不是人去适应机器,而是机器主动来学习和适应人类,并同人类一起学习和创新这个世界。“人工智能”正式写入2017年政府工作报告,折射出未来人工智能产业在我国经济发......一起来看看 《智能革命》 这本书的介绍吧!

在线进制转换器
在线进制转换器

各进制数互转换器

随机密码生成器
随机密码生成器

多种字符组合密码

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具