KOA 与 CO 实现浅析

KOA 与 CO 的实现都非常的短小精悍,只需要花费很短的时间就可以将源代码通读一遍。以下是一些浅要的分析。

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

既然 KOA 实现了 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 就是对上面这段代码的封装。

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

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

先关注两个函数:

// 构造函数    
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 服务器的建立过程:

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

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

我们细看下 app.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);
  }
};

先跳过 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 的原因。

原文地址:https://www.cnblogs.com/bingooo/p/6005359.html