前言
好久没有写博客了,一方面是工作了比较忙,一方面是觉得之前的博客都是一个小点写成一篇文章,没有比较大的点,最近刚好在看 Koa 源码,就把这个写成了文章。
整体概况
Koa 很精简,总共有四个文件:application.js、context.js、request.js、response.js,其他一些工具函数都写成一个包了,方便其他人使用。
application.js
application.js 是整个 Koa 的入口文件,我们从这个文件开始,解析 Koa 源码。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22;
/**
 * Module dependencies.
 */
const isGeneratorFunction = require('is-generator-function'); // 用来判断一个函数是否 generator
const debug = require('debug')('koa:application'); // 引入 debug 包,并且为 debug 设置输出的命名空间
const onFinished = require('on-finished'); // 当一个 http 请求 closes, finishes, 或者 errors 时,调用回调函数
const response = require('./response'); // 引入 response 文件
const compose = require('koa-compose'); // compose 主要是将中间件组合起来,形成 ‘洋葱模型’
const isJSON = require('koa-is-json'); // 判断一个 response 的 body 是否是 json
const context = require('./context'); // 引入 context 文件
const request = require('./request'); // 引入 request 文件
const statuses = require('statuses'); // 判断 http 的状态码
const Emitter = require('events'); // node 的 events 提供了一个订阅发布模式
const util = require('util'); // node 的 util 包
const Stream = require('stream'); // node 的 stream 处理流数据的抽象接口
const http = require('http'); // node 的 http 包
const only = require('only'); // only 函数返回对象的指定键值
const convert = require('koa-convert'); // 将 generator 函数转成 promise 函数
const deprecate = require('depd')('koa'); // 给出标记为废弃的信息
一开始用了 use strict,开启严格模式,具体的作用可以看这篇文章
)
然后就是一堆引入,这里打了注释,告诉你这个包是用来干嘛的。
接下来我们来看大概:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93module.exports = class Application extends Emitter {
  /**
   * Initialize a new `Application`.
   *
   * @api public
   */
  constructor() {...}
  /**
   * Shorthand for:
   *
   *    http.createServer(app.callback()).listen(...)
   *
   * @param {Mixed} ...
   * @return {Server}
   * @api public
   */
  listen(...args) {...}
  /**
   * Return JSON representation.
   * We only bother showing settings.
   *
   * @return {Object}
   * @api public
   */
  toJSON() {...}
  /**
   * Inspect implementation.
   *
   * @return {Object}
   * @api public
   */
  inspect() {...}
  /**
   * Use the given middleware `fn`.
   *
   * Old-style middleware will be converted.
   *
   * @param {Function} fn
   * @return {Application} self
   * @api public
   */
  use(fn) {...}
  /**
   * Return a request handler callback
   * for node's native http server.
   *
   * @return {Function}
   * @api public
   */
  callback() {...}
  /**
   * Handle request in callback.
   *
   * @api private
   */
  handleRequest(ctx, fnMiddleware) {...}
  /**
   * Initialize a new context.
   *
   * @api private
   */
  createContext(req, res) {...}
  /**
   * Default error handler.
   *
   * @param {Error} err
   * @api private
   */
  onerror(err) {...}
};
/**
 * Response helper.
 */
function respond(ctx) {...}
可以看到第一行1
module.exports = class Application extends Emitter
继承了 Emitter 这个包,就是用了 node 的发布订阅模式,一般用于监听 error 事件。
现在我们通过官网一个最简单的例子来解析源码:1
2
3
4
5
6
7
8const Koa = require('koa');
const app = new Koa();
app.use(async ctx => {
  ctx.body = 'Hello World';
});
app.listen(3000);
首先第一步引入 Koa,然后就是 const app = new Koa(),这一步会创建一个 Koa 实例,执行  constructor 方法。
让我们看看 constructor:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20/**
 * Initialize a new `Application`.
 *
 * @api public
 */
constructor() {
    super();
    this.proxy = false; // 这个是给 request.js 用的
    this.middleware = []; // 中间件数组
    this.subdomainOffset = 2; // 这个是给 request.js 用的
    this.env = process.env.NODE_ENV || 'development'; // 环境
    this.context = Object.create(context); // 创建 context 对象
    this.request = Object.create(request); // 创建 request 对象
    this.response = Object.create(response); // 创建 response 对象
    if (util.inspect.custom) {
      // 这里自定义 util 包的 inspect 方法
      this[util.inspect.custom] = this.inspect;
    }
  }
既然看到 inspect 方法了,那我们就看看具体:1
2
3
4
5
6
7
8
9
10/**
 * Inspect implementation.
 *
 * @return {Object}
 * @api public
 */
inspect() {
  return this.toJSON();
}
inspect 就是调用了 toJson 方法,看一下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15/**
 * Return JSON representation.
 * We only bother showing settings.
 *
 * @return {Object}
 * @api public
 */
toJSON() {
  return only(this, [
    'subdomainOffset',
    'proxy',
    'env'
  ]);
}
就是用到了一开始引用的 only 包,返回对象的 subdomainOffset,proxy,env 三个值。其中 proxy 表示是否开启代理,默认为 false 如果开启代理,对于获取 request 请求中的 host ,protocol,ip 分别优先从 Header 字段中的 X-Forwarded-Host,X-Forwarded-Proto,X-Forwarded-For 获取。subdomainOffset的意思是子域名的偏移。假设域名是”http://tobi.ferrets.example.com"。如果 app.subdomainOffset 没有设置,也就是说默认要忽略的偏移量是2,那么 ctx.subdomains 是["ferrets", "tobi"],如果设置为3,那么结果是 ["tobi"]。
1  | const Koa = require('koa');  | 
接下来,第三行1
2
3app.use(async ctx => {
  ctx.body = 'Hello World';
});
调用了 use 方法,那我们看看 use:1
2
3
4
5
6
7
8
9
10
11
12use(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;
}
首先判断参数 fn 是否为函数,不是就报错。然后判断 fn 是否为 generator 函数,是的话就输出即将要废弃的提示,接着把 fn,用引入的 convert 包,将 generator 转成 promise。然后输出 debug 信息,最后 push 到中间件数组 middleware,返回 this。
最后的一句话1
app.listen(3000);
用到了 listen 方法,看看:1
2
3
4
5listen(...args) {
    debug('listen');
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }
输出 debug 信息,然后调用 http 包的 createServer 方法,传入 callback 函数,最后调用 server.listen,传入 args 参数,启动 HTTP 服务器监听连接。其中调用了 callback,这个 callback 方法是 application.js 的核心:1
2
3
4
5
6
7
8
9
10
11
12callback() {
  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;
}
这个函数也很简单,就是调用 compose 方法,把 middleware 组合起来,compose 这个方法还是很重要的,下面会讲到,先跳过。然后判断一下有没有监听 error 这个事件,没有就监听,用了 onerror 方法 ,接下来返回一个函数:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21return (req, res) => {
  const ctx = this.createContext(req, res);
  return this.handleRequest(ctx, fn);
 };
``` 
这个函数首先调用 `createContext` 方法,把参数 `req`、`res`传进去:
```js
  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;
  }
就是每次请求都会创建一个 context,然后将 Node 的 request 对象和 response 对象赋值给对应的属性,然后返回 context 给 handleRequest 使用。handleRequest :1
2
3
4
5
6
7
8handleRequest(ctx, fnMiddleware) {
  const res = ctx.res;
  res.statusCode = 404;
  const onerror = err => ctx.onerror(err);
  const handleResponse = () => respond(ctx);
  onFinished(res, onerror);
  return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
这个函数的作用就是执行中间件,执行完之后调用 respond 方法。首先接收上面的 context 和 已经转成中间件的 fnMiddleware,fnMiddleware 经过 compose 处理之后其实是长这样:1
fnMiddleware = function (context, next) {...}
然后设置默认的 statusCode 为 404,然后调用 onFinished,监听 res 结束,执行完所有中间件的时候,调用 respond,这个 respond 方法并没有在 Application 类里面,而是外面的方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58function respond(ctx) {
  // allow bypassing koa
  if (false === ctx.respond) return;
  // 检查是否可写
  if (!ctx.writable) return;
  const res = ctx.res;
  let body = ctx.body;
  const code = ctx.status;
  // ignore body
  // 用 statuses 包判断 code 是否为 body 为空的类型,如 204,205 等
  if (statuses.empty[code]) {
    // strip headers
    // 将 body 置空
    ctx.body = null;
    return res.end();
  }
  // 如果 method 是 HEAD 类型
  if ('HEAD' == ctx.method) {
    // headersSent 表示响应头是否被发送出去了
    // 如果头部没有被发送,就设置 length
    if (!res.headersSent && isJSON(body)) {
      ctx.length = Buffer.byteLength(JSON.stringify(body));
    }
    return res.end();
  }
  // status body
  if (null == body) {
    // 判断 Http 协议的版本
    if (ctx.req.httpVersionMajor >= 2) {
      body = String(code);
    } else {
      // ctx.message 在这里其实是 ctx.req.message,Http 的 req,res 都有 message 字端
      body = ctx.message || String(code);
    }
    if (!res.headersSent) {
      ctx.type = 'text';
      ctx.length = Buffer.byteLength(body);
    }
    return res.end(body);
  }
  // responses
  // 根据 body 不同的类型做不同的处理
  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);
}
context.js
这个文件主要是用 delegates 这个包,将 response 和 request 上面的属性、方法代理到 context 上,比如:this.response. headerSent 就等于 this.ctx.headerSent
举个例子:delegate(proto, 'response').getter('headerSent')1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22function Delegator(proto, target) {
  if (!(this instanceof Delegator)) return new Delegator(proto, target);
  this.proto = proto;
  this.target = target;
  this.methods = [];
  this.getters = [];
  this.setters = [];
  this.fluents = [];
}
Delegator.prototype.getter = function(name){
  var proto = this.proto;
  var target = this.target;
  this.getters.push(name);
  proto.__defineGetter__(name, function(){
    return this[target][name];
  });
  return this;
};
...
源码很简单,用了 proto.__defineGetter__ 这个被废弃的方法,现在一般都用 Object. defineProperty 或者定义  getter,setter
request.js
这里引用网上一张图
                
response.js
这里也引用网上一张图
                
compose
这个方法可以算是核心了,把中间件组合起来形成洋葱模型:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/**
 * Expose compositor.
 */
module.exports = compose
/**
 * Compose `middleware` returning
 * a fully valid middleware comprised
 * of all those which are passed.
 *
 * @param {Array} middleware
 * @return {Function}
 * @api public
 */
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
   */
  return function (context, next) {
    // last called middleware #
    // 这里 index 是正在执行的中间件的下标
    let index = -1
    return dispatch(0)
    function dispatch (i) {
      // 判断中间件的 next 函数调用多次
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      // 获取中间件
      let fn = middleware[i]
      // 如果已经到最后一个中间件,那么最后执行就是传入的 next 函数
      if (i === middleware.length) fn = next
      // 如果函数没有定义,那么直接 resolve
      if (!fn) return Promise.resolve()
      try {
        // 执行中间件
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}
源码很简单,先是判断传入的参数是否为函数数组,然后返回一个函数,接收context,next 两个参数,然后返回dispatch 函数,这个函数才是真正的执行传入的函数。对于一个中间件多次调用 next() 会报错,举个例子看看:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21async function first(ctx, next) {
  console.log('1');
  await next();
  await next(); // 两次调用 next
  console.log(ctx);
};
async function second(ctx, next) {
  console.log('2');
  await next();
};
const middleware = [first, second];
const com = compose(middleware);
const hello = function() {
  console.log('hello');
}
com('ctx', hello);
compose 把中间件串起来,结合上面的源码可知,com 就是一个函数,接收 context 和 next 两个参数,然后开始执行。
第一次执行,index = -1, i = 0, index < i 不会报错,继续执行,index = i = 0,fn = middleware[i] = middleware[0] = first,i !== middleware.length,fn 有定义,执行 try 的东西,Promise.resolve(fn(context, dispatch.bind(null, i + 1))); fn = first即 Promise.resolve(first(context, dispatch.bind(null, i + 1))); 执行 first,函数输出 1,并调用第一个 next,这里的 next 就是 dispatch.bind(null, 0 + 1),再次调用 dispatch。此时 first 未执行完,只是中途跳出去执行下一个函数了,这就是洋葱模型。
第二次执行,index = 0, i = 1, index < i 不会报错,继续执行,index = i = 1,fn = middleware[i] = middleware[1] = second,i !== middleware.length,fn 有定义,执行 try 的东西,Promise.resolve(fn(context, dispatch.bind(null, i + 1))); fn = second即 Promise.resolve(second(context, dispatch.bind(null, i + 1))); 执行 second,函数输出 2,并调用 next,这里的 next 就是 dispatch.bind(null, 1 + 1),再次调用 dispatch。
第三次执行,index = 1, i = 2, index < i 不会报错,继续执行,index = i = 2,fn = middleware[i] = middleware[2],i === middleware.length,fn === next,这个 next 就是一开始 com 传入的 hello,Promise.resolve(fn(context, dispatch.bind(null, i + 1))); fn = hello即 Promise.resolve(hello(context, dispatch.bind(null, 2 + 1)));执行 hello,函数输出 hello,这个相当于洋葱模型的最中心,hello 函数没有调用 next,执行完之后就开始返回上一个函数。
返回 second,second 执行完毕。
返回 first,接下来还有第二个 next(),那就开始执行 next。
第四次执行,由于在第一次执行的时候,next 就是 dispatch.bind(null, 0 + 1),再次调用 dispatch 时,index = 2,i = 1,此时 i <= index,报错。这样就可以判断 next 函数调用多次。
借着这个例子,把 compose 整个流程都过了一遍,对大家理解中间件的执行有更好的帮助。
总结
Koa 短小精悍,短短几百行的核心代码,就提供了一套优雅的扩展性强的方法,值得学习学习。
上面内容如有错误,欢迎大家指出。
参考: