阅读 815

Koa2 源码学习(上)

引言

最近读了一下Koa2的源码;在阅读Koa2 (2.3.0) 的源码的过程中,我的感受是整个代码设计精巧,思路清晰,是一个小而精的 nodejs web服务框架。

设计理念

作为web服务框架,都是要围绕核心服务而展开的。那什么是核心服务呢?其实就是接收客户端的一个http的请求,对于这个请求,除了接收以外,还有解析这个请求。所以说会有

HPPT:接收 -> 解析 -> 响应

在响应客户端的时候,也有很多种方式,比如返回一个html页面,或者json文本。在解析请求和响应请求的中间,会有一些第三方的中间件,比如 日志、表单解析等等来增强 koa 的服务能力,所以 koa 至少要提供 "请求解析"、"响应数据"、"中间件处理" 这三种核心能力的封装,同时还需要有一个串联他们执行环境的上下文(context)

  • HTTP
  • 接收
  • 解析
  • 响应
  • 中间件
  • 执行上下文

上下文可以理解为是http的请求周期内的作用域环境来托管请求响应和中间件,方便他们之间互相访问。

以上分析是站在单个http请求的角度来看一个web服务能力。那么站在整个网站,站在整个后端服务的角度来看的话,能够提供 "请求"、"响应"、"解析"、"中间件"、"http流程全链路" 这些服务能力的综合体,可以看做是一个应用服务对象。如果把这些全放到 koa 里的话,那么对应的就是:

  • Application
  • Context
  • Request
  • Response
  • Middlewares
  • Session
  • Cookie

Koa的组成结构

首先看下koa的目录结构

  • application.js:框架入口;负责管理中间件,以及处理请求
  • context.js:context对象的原型,代理request与response对象上的方法和属性
  • request.js:request对象的原型,提供请求相关的方法和属性
  • response.js:response对象的原型,提供响应相关的方法和属性
// application.js

const isGeneratorFunction = require('is-generator-function'); // 判断当前传入的function是否是标准的generator function
const debug = require('debug')('koa:application'); // js调试工具
const onFinished = require('on-finished'); // 事件监听,当http请求关闭,完成或者出错的时候调用注册好的回调
const response = require('./response'); // 响应请求
const compose = require('koa-compose'); // 中间件的函数数组
const isJSON = require('koa-is-json'); // 判断是否为json数据
const context = require('./context'); // 运行服务上下文
const request = require('./request'); // 客户端的请求
const statuses = require('statuses'); // 请求状态码 
const Cookies = require('cookies');
const accepts = require('accepts'); // 约定可被服务端接收的数据,主要是协议和资源的控制
const Emitter = require('events'); // 事件循环
const assert = require('assert'); // 断言
const Stream = require('stream');
const http = require('http');
const only = require('only'); // 白名单选择
const convert = require('koa-convert'); // 兼容旧版本koa中间件
const deprecate = require('depd')('koa'); // 判断当前在运行koa的某些接口或者方法是否过期,如果过期,会给出一个升级的提示
复制代码

以上是koa入口文件的依赖分析。接下来我们进行源码分析,首先我们利用删减法来筛出代码的核心实现即可,不用上来就盯细节! 我们只保留constructor

// application.js

module.exports = class Application extends Emitter {
  constructor() {
    super();

    this.proxy = false; // 是否信任 proxy header 参数,默认为 false
    this.middleware = []; //保存通过app.use(middleware)注册的中间件
    this.subdomainOffset = 2; // 子域默认偏移量,默认为 2
    this.env = process.env.NODE_ENV || 'development'; // 环境参数,默认为 NODE_ENV 或 ‘development’
    this.context = Object.create(context); //context模块,通过context.js创建
    this.request = Object.create(request); //request模块,通过request.js创建
    this.response = Object.create(response); //response模块,通过response.js创建
  }

  // ...
}
复制代码

我们可以看到,这段代码暴露出一个类,构造函数内预先声明了一些属性,该类继承了Emitter,也就是说这个类可以直接为自定义事件注册回调函数和触发事件,同时可以捕捉到其他地方触发的事件。

除了这些基本属性之外,还有一些公用的api,最重要的两个一个是==listen==,一个是==use==。koa的每个实例上都会有这些属性和方法。

// application.js

module.exports = class Application extends Emitter {
  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);
  }

  listen() {
    const server = http.createServer(this.callback());
    return server.listen.apply(server, arguments);
  }

  use(fn) {
    this.middleware.push(fn);
    return this;
  }
}
复制代码

listen 方法内部通过 http.createServer 创建了一个http服务的实例,通过这个实例去 listen 要监听的端口号,http.createServer 的参数传入了 this.callback 回调

// application.js

module.exports = class Application extends Emitter {
  ...
  callback() {
    const fn = compose(this.middleware); // 把所有middleware进行了组合,使用了koa-compose

    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn); // 返回了本身的回调函数
    };

    return handleRequest;
  }
}
复制代码

可以看到,handleRequest 返回了本身的回调,接下来看 handleRequest 。

handleRequest 方法直接作为监听成功的调用方法。已经拿到了 包含 req res 的 ctx 和可以执行所有中间件函数的 fn。 首先一进来默认设置状态码为==404== . 然后分别声明了 成功函数执行完成以后的成功 失败回调方法。这两个方法实际上就是再将 ctx 分化成 req res。 分别调这两个对象去客户端执行内容返回。 ==context.js request.js response.js== 分别是封装了一些对 ctx req res 操作相关的属性,我们以后再说。

// application.js

module.exports = class Application extends Emitter {
  ...
  handleRequest(ctx, fnMiddleware) {
    const res = ctx.res; // 拿到context.res
    res.statusCode = 404; // 设置默认状态吗404
    const onerror = err => ctx.onerror(err); // 设置onerror触发事件
    const handleResponse = () => respond(ctx); // 向客户端返回数据
    onFinished(res, onerror);
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }
}
复制代码

失败执行的回调

onerror(err) {
  assert(err instanceof Error, `non-error thrown: ${err}`);

  if (404 == err.status || err.expose) return;
  if (this.silent) return;

  const msg = err.stack || err.toString();
  console.error();
  console.error(msg.replace(/^/gm, '  '));
  console.error();
}
复制代码

成功执行的回调

function respond(ctx) {
  ...
}
复制代码

return fnMiddleware(ctx).then(handleResponse).catch(onerror); 我们拆分理解,首先 return fnMiddleware(ctx) 返回了一个中间件数组处理链路,then(handleResponse) 等到整个中间件数组全部完成之后把返回结果通过 then 传递到 handleResponse。

// application.js

module.exports = class Application extends Emitter {
  ...
  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.cookies = new Cookies(req, res, {
      keys: this.keys,
      secure: request.secure
    });
    request.ip = request.ips[0] || req.socket.remoteAddress || '';
    context.accept = request.accept = accepts(req);
    context.state = {};
    return context;
  }
}
复制代码

这里我们不用去太深入去抠代码,理解原理就行。createContext 创建 context 的时候,还会将 req 和 res 分别挂载到context 对象上,并对req 上一些关键的属性进行处理和简化 挂载到该对象本身,简化了对这些属性的调用。我们通过一张图来直观地看到所有这些对象之间的关系。

  • 最左边一列表示每个文件的导出对象
  • 中间一列表示每个Koa应用及其维护的属性
  • 右边两列表示对应每个请求所维护的一些列对象
  • 黑色的线表示实例化
  • 红色的线表示原型链
  • 蓝色的线表示属性

createContext 简单理解就是挂载上面的对象,方便整个上下游http能及时访问到进出请求及特定的行为。

// application.js

module.exports = class Application extends Emitter {
  ...
}
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; // 赋值服务状态码

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

  // 通过判断body类型来调用,这里的res.end就是最终向客户端返回数据的动作
  if (Buffer.isBuffer(body)) return res.end(body);
  if ('string' == typeof body) return res.end(body);
  if (body instanceof Stream) return body.pipe(res);

  // 返回为json数据
  body = JSON.stringify(body);
  if (!res.headersSent) {
    ctx.length = Buffer.byteLength(body);
  }
  res.end(body);
}
复制代码

respond 函数是 handleRequest 成功处理的回调,内部做了合理性校验,诸如状态码,内容的类型判断,最后向客户端返回数据。

结语

以上就是我们对application.js文件的分析,通过上面的分析,我们已经可以大概得知Koa处理请求的过程:当请求到来的时候,会通过 req 和 res 来创建一个 context (ctx) ,然后执行中间件。

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