SunShinewyf/issue-blog

koa源码解读

SunShinewyf opened this issue · 0 comments

继上次对express进行简单地了解和深入之后,又开始倒腾koa了。对于koa的印象是极好的,简洁而有表现力。和express相比它有几个比较明显的特征:

  • 比较新潮,koa1中使用了generator,拥抱es6语法,使用同步语法来避免callback hell的层层嵌套。在koa2中又拥抱了es7async-await语法,语法表现形式更为简洁。
  • 变得更轻量化,相比于expresskoa抽离了原先内置的中间件,一些比较重要的中间件都使用单独的中间件插件,使得开发者可以根据自己的实际需要使用中间件,更加灵活。就比如给开发者建造了一个简单的地基,之后的装修设计都由开发者自己决定,精简灵活。
    关于更多更详细的两者以及和hapi的比较,读者可以移步这里

在源码方面,koa变得更加轻量化,但是还是很有特点的。目录结构如下:

- lib/
    - application.js
    - context.js
    - request.js
    - response.js

从目录结构来看,只有四个文件,摒弃了express中的路由模块,显得简单而有表现力。四个文件中分别定义了四个对象,分别是app对象,context,request以及response。深入源码查看,你会发现更加简单,每个文件的代码行数也是很少,而且逻辑嵌套并不复杂。

中间件原理

首先定义了一个构造函数,源码如下:

function Application() {
  if (!(this instanceof Application)) return new Application;
  this.env = process.env.NODE_ENV || 'development';
  this.subdomainOffset = 2;
  this.middleware = [];
  this.proxy = false;
  this.context = Object.create(context);  //koa的上下文对象
  this.request = Object.create(request);  //koa.request
  this.response = Object.create(response);  //koa.request
}

在这段里面只是单纯地定义了一个实例化app的一些属性。如上下文,中间件数组等。
然后是注册了一些中间件,中间件的use源码很简单,就是将当前的中间件函数push到该应用实例的middleware数组中,在此不再赘述。
最后定义了开启服务的lisen函数,在这个函数里面没有什么特殊的,只是有一点需要注意:它将自身原型的一个callback作为参数传入:

  var server = http.createServer(this.callback());

这一句很关键,它表示每次在开启koa服务的时候,就会执行传入的callback,而callback都干了些啥,具体看源码:

app.callback = function(){
  if (this.experimental) {
    console.error('Experimental ES7 Async Function support is deprecated. Please look into Koa v2 as the middleware signature has changed.')
  }
  var fn = this.experimental
    ? compose_es7(this.middleware)
    : co.wrap(compose(this.middleware));
  var self = this;

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

  return function handleRequest(req, res){
    res.statusCode = 404;
    var ctx = self.createContext(req, res);
    onFinished(res, ctx.onerror);
    fn.call(ctx).then(function handleResponse() {
      respond.call(ctx);
    }).catch(ctx.onerror);
  }
};

在这个函数里面,通过experimental参数作为是否使用es7async的标准,当然了,这种折中处理的方式是koa1中的,在koa2中,由于完全摒弃了generator,转而拥抱async-await,所以直接使用的const fn = compose(this.middleware);就简单进行处理了。对于使用es7语法的情况,使用的compose_es7app中的中间件数组进行处理:

function compose(middleware) {
  return function (next) {
    next = next || new Wrap(noop);
    var i = middleware.length;
    while (i--) next = new Wrap(middleware[i], this, next);
    return next
  }
}

也就是将中间件进行遍历,compose函数的作用如下:

compose([f1,f2,...,fn])(args)  =====>  f1(f2(f3(..(fn(args)))));  

也就是将数组里面的函数依次执行,通过一个next中间值不断将执行权进行传递。如果传入的中间件数组不是generator函数,那么应该是依次执行,但是generator有暂停执行的功能,所以一旦执行yield next的时候,就会去执行下一个函数。等下一个中间件执行完成时,再在原来中断的地方继续执行。这种执行方式导致形成了koa中著名的洋葱模型。
输入图片说明

举例子如下:

first step before
second step before
second step after
first step after

当使用es7语法时,处理也是一样的。

上下文context对象

相比在express中,koa多了一个上下文对象,创建上下文的源码如下:

app.createContext = function(req, res){
  var context = Object.create(this.context);
  var request = context.request = Object.create(this.request);
  var 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.onerror = context.onerror.bind(context);
  context.originalUrl = request.originalUrl = req.url;
  context.cookies = new Cookies(req, res, {
    keys: this.keys,
    secure: request.secure
  });
  context.accept = request.accept = accepts(req);
  context.state = {};
  return context;
};

从中可以看出,ctx.reqctx.res代表的是noderequestresponse对象,而ctx.requestctx.response则代表的是koa的对应对象。在express中,获取一些参数是通过访问传入的res或者req的对应参数。但是在koa中,则是访问的ctx上下文里面的相应参数。只是两者的封装不一样而已。

以上是我的一些分析,不对的地方希望大神交流和指出。