koa2一部分源码解析

koa2 source code analysis

Posted by markzhang on May 20, 2019

从18年下半年以来开始涉足node开发以来,算起来使用koa2也快接近一年了。在使用koa2过程中对于项目由陌生到熟悉,于是想着写一篇总结来归纳自己对于koa2的理解和认识。

这篇文章会分析以下四个模块:中间件机制、request对象、response对象、context对象。

中间件机制

我们通常在项目是通过以下方式来使用中间件的

1
2
3
4
let bodyParser = require('koa-bodyparser')
app.use(bodyParser({
  forLimit: '4mb'
}));

我们可以猜想到use这个函数的作用是对要运行的中间件进行收集,那让我们来看一下源码

1
2
3
4
5
6
7
8
9
10
11
12
13
function use(fn) {
  if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
  // 新版本的koa使用了async wait的语法,所以检测generator函数进行转化,并引导用户使用新的语法
  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);
  }
  // this.middleware是一个数组,存放中间件函数
  this.middleware.push(fn);
  return this;
}

在收集到中间件之后,自然要对其进行组织,在koa2里面使用的koa-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
function compose(middleware) {
  if (!Array.isArray(middleware)) throw new TypeError('Middleware must be an array!');
  for (const fn of middleware) {
    if (typeof fn != 'function') {
      throw new TypeError('Middleware must be composed of functions!');
    }
  }
  return function(context, next) {
    let index = -1;
    return dispatch(0);
    function dispatch(i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'));
      index = i;
      let fn = middleware[i];
      if(i === middleware.length) fn = next;
      if (!fn) return Promise.resolve();
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject();
      }
    }
  }
}

/**
 * 从上面函数来看,compose函数里面相当于是绑定了中间件的执行关系
 * 中间件函数调用是在具体的中间件中执行的
 * koa中间件之所以能按照洋葱圈的模式运行,是因为每一个await next()相当于是跑到了下一个中间件去执行代码
 * 一直要到最后一个中间件执行完毕,最深层的await拿到返回值,才会一层一层返回最初的中间件,有点类似于递归。
**/
// 看下面的例子
let a = async (ctx, netx) => {
  console.log(1);
  await next();
  // 1、这两个函数相当于是同一个compose函数里面得到的dispatch(null, i + 1)
  // 2、为什么不允许存在多次next调用呢,因为洋葱圈只执行一次,执行多次没有意义
  // 3、洋葱圈模型也只是js的调用过程
  // 4、每一个netx都是下一个中间件
  // await next();
  console.log(4);
}

let b = async (ctx, next) => {
  console.log(2);
  await next();
  console.log(3);
}
// 执行之后打印出1 2 3 4

通过查看上面的代码中我们已经清晰的梳理出koa2中间件的收集和运行模式啦。。。

request对象和response对象

通过阅读request.js和response.js可以发现在这里面完成了一些简便操作,让我们在使用过程中可以轻松的从node原生request和response对象中获取和设置属性。如下分别是request.js和response.js的源码截图:

context对象

request对象和response对象可以看作是对于原生node的request对象和response对象的代理,通过阅读context.js的代码,我觉得我们也可以把context上下文对象理解为request对象和response对象的代理,并加上了框架对于错误处理的一些操作。我们先来看一下context对象的产生过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
 * 这个函数在application.js里面
 * 每次请求进来都会调用一次createContext函数,所以每个请求的context都是唯一的
 * 通过这个函数将request对象和response对象注入到了context对象里面
 * 然后在这个对象内部再次进行代理(即代理request和response对象的一些属性)
**/
function 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.js内部是如何完成代理的,答案如下 既然是使用了delegates这个包,让我们深挖一下这个包里做了什么吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function 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.method = function(name) {
  var proto = this.proto;
  var target = this.target;
  this.methods.push(name);

  // 重点就是在这里,意思就是在context对象上获取某些属性的时候,进入代理对象里面去获取相应的值
  proto[name] = function() {
    return this[target][name].apply(this[target], arguments);
  }

  return this;
}

以上大概就是本次koa2一部分源码解析的全部内容啦,我们可以看到koa2是一个非常简洁的框架,但是却通过巧妙的中间件收集和执行方式让它有了强大的扩展能力,我想这也是它受到广大开发者喜爱的原因之一吧。同时我在想的是,作为开发者我们到底是需要一个简洁的框架还是一个功能模块相对完善的框架,我想这个没有标准答案,不同项目有不同的考虑,但是我想在对一个项目进行架构的时候,参考现有优秀成熟的项目总是有必要的。