Koa 梳理分析【一:koa 实例】

1,157 阅读4分钟

之前梳理Redux的时候,说到它的中间件的处理方式与koa是一样的,所以就想到那就把koa也看一遍吧,梳理一遍吧。koa非常的简洁适合阅读,该文章分析的当前版本为2.8.1

目录结构

通过github上的package.json可以看到,koa的入口文件是lib/application.js。整个lib才四个文件,当然里面引入了一些其他的工具函数。

── lib
   ├── application.js
   ├── context.js
   ├── request.js
   └── response.js

下面从程序入口出发,也就是application.js

application.js

先看一下关键的相关依赖:

const response = require('./response');
const context = require('./context');
const request = require('./request');
const compose = require('koa-compose');
const http = require('http');
const Emitter = require('events');
...

可以看到,入口文件把其他三个js文件都引入进来,然后主要引入了httpkoa-composeevents,其他的我先省略了。koa-compose库主要是为了对koa的中间件进行合并的工具函数,其他两个都是node的标准库。

该文件使用module.exports = class Application extends Emitter {...} 导出了koa类。这里看到Application 继承了 Emitter,所有它也包含了异步事件的处理能力,后面可以看到koa的错误处理会使用到Emitter提供的事件模型方法。

构造函数

constructor(options) {
    super();
    options = options || {};
    this.proxy = options.proxy || false;
    this.subdomainOffset = options.subdomainOffset || 2;
    this.env = options.env || process.env.NODE_ENV || 'development';
    if (options.keys) this.keys = options.keys;
    this.middleware = [];
    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;
    }
}

重点可以看到:

  • 声明和初始化了 middleware 属性,是用来存放之后添加的中间件的。
  • 使用Object.create方法,分别创建了一个对象,这些对象的原型分别指向contextrequestresponse,分别对应最开始引入的其他三个js文件。

到这里,先写一份使用koa的使用的示例代码,主要引导整个处理流程:

const Koa = require('koa')

const app = new Koa()

app.use(async (ctx, next) => {
  console.log('@@ start 1')
  next()
  console.log('@@ end 1')
})

app.use(async (ctx, next) => {
  console.log('@@ start 2')
  next()
  console.log('@@ end 2')
})

app.listen(3000)

use

从上往下,当执行new Koa()的时候,也就是调用上面说的构造函数。来到app.use添加中间件的时候:

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

use函数会判断传入的中间件如果是generator,就会使用convert函数去将生成器转成类async/await函数,这个兼容会在3.*之后去掉。核心的一步就是将函数添加到了this.middleware队列里面。

listen

这里就是正式的使用http库提供的createServer方法来创建一个web服务了,并且监听相应的端口。

listen(...args) {
    debug('listen');
    const server = http.createServer(this.callback());
    return server.listen(...args);
}

根据http.createServer的文档和使用,可以肯定这里的this.callback()函数执行会返回一个 (req, res) => {} 这样的函数。

callback

callback() {
    const fn = compose(this.middleware);

    if (!this.listenerCount('error')) this.on('error', this.onerror);

    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn);
    };

    return handleRequest;
}

首先就将已经注册的中间件进行了合并,这里就是经典的洋葱模型中间件机制。然后判断了一下是否有过错误监听,没有的话就添加一个,这里利用了Emitter的事件模型,最后返回一个http.createServer能够使用的回调函数。

到这里,整个服务的启动流程已经结束。

处理请求

当服务收到请求,最后执行的就是传入http.createServer的回调函数。

handleRequest = (req, res) => {
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn);
};

在处理请求之前,首先使用this.createContext将请求和响应对象进行了聚合封装成了ctx对象,然后再交给handleRequest函数去处理。

createContext

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

首先通过Object.create函数去创建了一个原型为this.context的空对象,之后就是为这个对象赋值了,可以看到在平时使用的时候访问的一些属性是怎么来的了,他们之间的关系是怎么样的可以很清楚的看见。

handleRequest

handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    res.statusCode = 404;
    const onerror = err => ctx.onerror(err);
    const handleResponse = () => respond(ctx);
    onFinished(res, onerror); // 当http的请求关闭,出现错误的时候,执行回调。
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}

默认给响应状态设置为404,然后创建一个统一错误处理的回调函数和响应处理函数函数。将ctx传给被合并后的中间件,然后使用thencatch分别来处理中间件等正常处理和异常监控。

respond

当请求经过了所有的中间件处理之后,在最后调用handleResponse方法,然后去执行respond函数,最终组织响应对象,进行服务响应。

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

看到body的类型支持BufferstringStreamJSON

小结

通过梳理application.js可以知道,它核心主要是做了这样几个事情:

  1. 通过http启动web服务,创建koa实例。
  2. 处理合并中间件为洋葱模型。
  3. 创建和封装高内聚的context。
  4. 实现异步函数的统一错误处理机制。

参考文章 可能是目前最全的koa源码解析指南 on-finished

原文链接,欢迎star