KOA 与 CO 实现浅析

KOA 与 CO 的实现都很是的短小精悍,只须要花费很短的时间就能够将源代码通读一遍。如下是一些浅要的分析。node

如何用 node 实现一个 web 服务器

既然 KOA 实现了 web 服务器,那咱们就先从最原始的 web 服务器的实现方式着手。
下面的代码中咱们建立了一个始终返回请求路径的 web 服务器。web

const http = require('http');
const server = http.createServer((req, res) => {
  res.end(req.url);
});
server.listen(8001);

当你请求 http://localhost:8001/some/url 的时候,获得的响应就是 /some/url数组

KOA 的实现

简单的说,KOA 就是对上面这段代码的封装。promise

首先看下 KOA 的大概目录结构:服务器

lib 目录下只有四个文件,其中 request.jsresponse.js 是对 node 原生的 request(req)response(res) 的加强,提供了不少便利的方法,context.js 就是著名的上下文。咱们暂时抛开这三个文件的细节,先看下主文件 application.js 的实现。app

先关注两个函数:dom

// 构造函数    
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);
  this.request = Object.create(request);
  this.response = Object.create(response);
}  
// listen 方法   
app.listen = function(){
  debug('listen');
  var server = http.createServer(this.callback());
  return server.listen.apply(server, arguments);
};

上面的这两个函数,正是完成了一个 web 服务器的创建过程:koa

const server = new KOA();  // new Application()
server.listen(8001);

而先前 http.createServer() 的那个回调函数则被替换成了 app.callback 的返回值。函数

咱们细看下 app.callback 的具体实现:oop

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

先跳过 ES7 的实验功能以及错误处理,app.callback 中主要作了以下几件事情:

  • 从新组合中间件并用 co 包装
  • 返回处理request的回调函数

每当服务器接收到请求时,作以下处理:

  • 初始化上下文
  • 调用以前 co.wrap 返回的函数,并作必要的错误处理

如今咱们把目光集中到这三行代码中:

// 中间件重组与 co 包装  
var fn = co.wrap(compose(this.middleware));
// ------------------------------------------  
// 在处理 request 的回调函数中  
// 建立每次请求的上下文  
var ctx = self.createContext(req, res);  
// 调用 co 包装的函数,执行中间件  
fn.call(ctx).then(function handleResponse() {
  respond.call(ctx);
}).catch(ctx.onerror);

先看第一行代码,compose 实际上就是 koa-compose,实现以下:

function compose(middleware){
  return function *(next){
    if (!next) next = noop();
    var i = middleware.length;
    while (i--) {
      next = middleware[i].call(this, next);
    }
    return yield *next;
  }
}
function *noop(){}

compose 返回一个 generator函数,这个 generator函数 中倒序依次以 next 为参数调用每一个中间件,并将返回的generator实例 从新赋值给 next,最终将 next返回。

这里比较有趣也比较关键的一点是:

next = middleware[i].call(this, next);

咱们知道,调用 generator函数 返回 generator实例,当 generator函数 中调用其余的 generator函数 的时候,须要经过 yield *genFunc() 显式调用另外一个 generator函数

举个例子:

const genFunc1 = function* () {
  yield 1;
  yield *genFunc2();
  yield 4;
}
const genFunc2 = function* () {
  yield 2;
  yield 3;
}
for (let d of genFunc1()) {
  console.log(d);
}

执行的结果是在控制台依次打印 1,2,3,4。

回到上面的 compose 函数,其实它就是完成上面例子中的 genFunc1 调用 genFunc2 的事情。而 next 的做用就是保存并传递下一个中间件函数返回的 generator实例

参考一下 KOA 中间件的写法以帮助理解:

function* (next) {
  // do sth.
  yield next;
  // do sth.
}

经过 compose 函数,KOA 把中间件所有级联了起来,造成了一个 generator 链。下一步就是完成上面例子中的 for-of循环的事情了,而这正是 co 的工做。

co 的原理分析

仍是先看下 co.wrap

co.wrap = function (fn) {
  createPromise.__generatorFunction__ = fn;
  return createPromise;
  function createPromise() {
    return co.call(this, fn.apply(this, arguments));
  }
};

该函数返回一个函数 createPromise,也就是 KOA 源码里面的 fn
当调用这个函数的时候,实际上调用的是 co,只是将上下文 ctx 做为 this 传递了进来。

如今分析下 co的代码:

function co(gen) {
  var ctx = this;
  var args = slice.call(arguments, 1)
  // 返回一个 promise
  return new Promise(function(resolve, reject) {
    if (typeof gen === 'function') gen = gen.apply(ctx, args);
    if (!gen || typeof gen.next !== 'function') return resolve(gen);
    
    onFulfilled();
    
    function onFulfilled(res) {
      var ret;
      try {
        ret = gen.next(res);
      } catch (e) {
        return reject(e);
      }
      next(ret);
    }

    function onRejected(err) {
      var ret;
      try {
        ret = gen.throw(err);
      } catch (e) {
        return reject(e);
      }
      next(ret);
    }

    function next(ret) {
      if (ret.done) return resolve(ret.value);
      var value = toPromise.call(ctx, ret.value);
      if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
      return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
        + 'but the following object was passed: "' + String(ret.value) + '"'));
    }
  });
}

co 函数的参数是 gen,就是以前 compose 函数返回的 generator实例

co 返回的 Promise 中,定义了三个函数 onFulfilledonRejectednext,先看下 next 的定义。

next 的参数实际上就是gen每次 gen.next() 的返回值。若是 gen 已经执行结束,那么 Promise 将返回;不然,将 ret.value promise 化,并再次调用 onFulfilledonRejected 函数。

onFulfilledonRejected 帮助咱们推动 gen 的执行。

nextonFulfilledonRejected 的组合,实现了 generator 的递归调用。那么到底是如何实现的呢?关键还要看 toPromise 的实现。

function toPromise(obj) {
  if (!obj) return obj;
  if (isPromise(obj)) return obj;
  if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
  if ('function' == typeof obj) return thunkToPromise.call(this, obj);
  if (Array.isArray(obj)) return arrayToPromise.call(this, obj);
  if (isObject(obj)) return objectToPromise.call(this, obj);
  return obj;
}

toPromise 函数中,后三个分支处理分别对 thunk 函数、数组和对象进行了处理,此处略去细节,只须要知道最终都调回了 toPromise 的前三个分支处理中。这个函数最终返回一个 promise 对象,这个对象的 resolvereject 处理函数又分别是上一个 promise 中定义的 onFulfilledonRejected 函数。至此,就完成了 compose 函数返回的 generator 链的推动工做。

最后还有一个问题须要明确一下,那就是 KOA 中的 context 是如何传递的。
经过观察前面的代码不难发现,每次关键节点的函数调用都是使用的 xxxFunc.call(ctx) 的方式,这也正是为何咱们能够在中间件中直接经过 this 访问 context 的缘由。

相关文章
相关标签/搜索