Skip to content
This repository has been archived by the owner on Nov 30, 2024. It is now read-only.

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

Open
bruce-16 opened this issue Aug 19, 2019 · 0 comments
Open

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

bruce-16 opened this issue Aug 19, 2019 · 0 comments

Comments

@bruce-16
Copy link
Owner

bruce-16 commented Aug 19, 2019

之前梳理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

@bruce-16 bruce-16 changed the title Koa 梳理分析【一】 Koa 梳理分析【一:koa 实例】 Aug 19, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

1 participant