Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Koa 源码浅析 #5

Open
yleo77 opened this issue Mar 1, 2016 · 0 comments
Open

Koa 源码浅析 #5

yleo77 opened this issue Mar 1, 2016 · 0 comments

Comments

@yleo77
Copy link
Owner

yleo77 commented Mar 1, 2016

简介

Koa 是一个非常小巧且轻量级的由 Node.js 完成的 web框架,底层借助于 co 解决了繁琐的回调嵌套,充分发挥了 Generator 的优势。目前该 team 已经尝试将 async/await 引入其中,不过还在 Alpha 阶段。

关于 Koa,还有一个关键字:Middleware。

这篇文章主要是在边看 Koa 源码边做的一个整理笔记。

如何使用

先通过代码看一段简单示例。

var koa = require('koa');
var app = koa();

// x-response-time
app.use(function *(next){
  var start = new Date;
  yield next;
  var ms = new Date - start;
  this.set('X-Response-Time', ms + 'ms');
});

// logger
app.use(function *(next){
  var start = new Date;
  yield next;
  var ms = new Date - start;
  console.log('%s %s - %s', this.method, this.url, ms);
});

// response
app.use(function *(){
  this.body = 'Hello World';
});

app.listen(3000);

如果细心看这段代码的话,对代码有一些直觉的同学到此就应该能大致了解一些,比如 app.use 中的函数会一个个执行,执行的方式大概是利用 yield next 触发的吧,next 那大概应该指向的是下一个中间件吧?

没错,是这样的。但是这就有一个疑问,如果 next 指向的是下一个中间件,那 next 的类型是什么,应该还是和 Generator 有关的吧,比如 Generator函数?比如 Iterator Object?

也没错。那可是在 Generator 内部,如果 yield 后面跟的还是 Generator,根据规范,这样是不会执行到内部 Generator 的吧?

恩,那 Koa 是怎么做到的呢?这个就当做 谜1 吧。

看完这篇文章就有答案了。

源码分析

考虑到篇幅,后续文中出现的源码会删掉不重要且不影响关键逻辑部分的代码,比如调试信息。

Koa 部分

通过上面的实例结合 koa 的两个最重要的 api 看源码。入口文件 lib/application.js

module.exports = Application    // exports 出来的是一个构造函数

原型链上有两个比较重要的 API,也是经常会看到的。

  • Application#use
  • Application#listen

listen 方法很简短,源码备注已经说的很明白了,就是

http.createServer(app.callback()).listen(...)

的简写,但这里有个方法需要后续了解一下,就是 Application#callback,这个放在 Application#use 之后讲,一步步来,先看 Application#use

Application#use 方法源码:

app.use = function(fn){
  if (!this.experimental) {
    // 如果没有打开`experimental`,那么 fn 必须为 generator
    assert(fn && 'GeneratorFunction' == fn.constructor.name, 'app.use() requires a generator function');
  }
  // 将 fn 压入 middleware 中,稍后会利用到,这是 koa 最重要的部分,需要多少个中间件,就多次使用 use 方法,传入 fn 即可。
  this.middleware.push(fn);
  return this;
};

现在来看 Application#callback

app.callback = function(){
  // 先对这些中间件做一次包装,包装是通过 compose 和 co 的方式完成的,具体实现方式放到最后再讲。
  // fn 的执行结果始终是一个 Promise.
  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);
  // 返回的是一个函数,也就是 httpCreateServer 的回调。
  return function(req, res){
    // 默认的 statusCode 值,之后会在 set body 中修正该值。
    res.statusCode = 404;
    // 创建this上下文,供后续使用。
    // ctx 上携带着大量 shortcut 的方法,例如req 上的accept,header 之类。
    var ctx = self.createContext(req, res);
    onFinished(res, ctx.onerror);
    // 中间件 和 respond 的执行都是在 ctx 这个上下文中。
    fn.call(ctx).then(function () {
      respond.call(ctx);
    }).catch(ctx.onerror);
  }
}

按从上到下,应该先看 Koa 是如何处理中间件的。但是这里需要投入大量的篇幅,而另外一个问题中间件执行完之后 respond 的源码实现相对来说就简短多了,只需要大概撇一眼即可,所以这里先看 respond 的实现,接下来再重点看 Koa 对中间件的处理。

// 这个方法没有太多需要介绍的。
function respond() {
  if (false === this.respond) return;

  var res = this.res;
  if (res.headersSent || !this.writable) return;

  var body = this.body;
  var code = this.status;

  // 有些 statuscode 可以不用设置 body,直接结束 res,比如204(reset content),205(partial content),304(not modified)
  if (statuses.empty[code]) {
    this.body = null;
    return res.end();
  }

  // 设置 `Content-Length`,正常情况下 length 的设置是在 this.body 中,但这个逻辑分支下需要修正。
  if ('HEAD' == this.method) {
    if (isJSON(body)) this.length = Buffer.byteLength(JSON.stringify(body));
    return res.end();
  }

  // 如果 body 为 null,则将 body 设置为statusMessage。
  if (null == body) {
    this.type = 'text';
    body = this.message || String(code);
    this.length = Buffer.byteLength(body);
    return res.end(body);
  }

  // 设置 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.stringify(body);
  this.length = Buffer.byteLength(body);
  res.end(body);
}

现在,重头戏才刚刚开始。这也是我认为的 koa 最有价值的地方,看看 koa 是如何处理这些中间件的。

  var fn = this.experimental
    ? compose_es7(this.middleware)
    : co.wrap(compose(this.middleware));

兵分两路,先看 compose_es7 的方式。但是在这之前,需要先介绍一个概念,compose。

Compose

通俗得来说 Compose 做的事情就是将多个函数组合成一个函数的过程。这里借用 Ramda 库的 compose 官方示例来说明它的作用。

var f = R.compose(R.inc, R.negate, Math.pow);
f(3, 4); // -(3^4) + 1

试想一下要完成这一个需求场景如果在不用 R.compose 库的时候会是什么样子。

R.inc(R.negate(Math.pow(3, 4)))

再试想一下在经常可以看到的业务场景之下的对比,对一串 JSON 做一些处理,比如根据 id 排序,根据日期过滤,摘取其中的 content 和 id 字段等等。

借助 compose

var f = R.compose(sort(item => item.id), 
  filter(item => item.date >= 20160101),
  map(item => {item.id, item.content})
  );
f(json);

不借助 compose,怎么写呢?这该用多少行代码才能做到并且还要在满足可复用性的前提下。

这样子的处理有什么好处呢?后者,数据混淆在逻辑之中,很难做到两者的分离,对于再复杂一点的业务来说,更是增加了抽象的难度。

而先通过 compose 的形式组合出一套完整的公式再等待数据的输入更有利于处理逻辑和数据的分离。这其实正是体现了函数式编程的思想所在:先有公式,再把数据塞给公式处理,返回结果,而不是算好一个值,再给另一个公式。公式有一个简单,那公式有多个怎么办,compose 一下变成一个。

这也是这里没有用 underscore 中 compose 的原因,underscore 虽然是函数式,但它总需要先传入数据,然后再有函数的链式调用,它更像是以容器为中心对操作的串联,更像是 jQuery 的变形,这个和真正的函数式有点脱节。

如果你有看过 Redux 源码的话,不知道有没有留意到 Redux 中applyMiddleWare部分也是借用了 compose 这一理念来实现的,这一部分,其实又和 Koa 是类似的。把一个个的中间件,经过 compose 组合成一条流水线(一个函数),然后将需要处理的数据传入这条流水线,顺次加工处理。

// redux 中 compose 的源码
// FYI,这部分也是我认为 Redux 处理的最漂亮的地方
function compose(...funcs) {
  return (...args) => {
    if (funcs.length === 0) {
      return args[0]
    }

    const last = funcs[funcs.length - 1]
    const rest = funcs.slice(0, -1)

    return rest.reduceRight((composed, f) => f(composed), last(...args))
  }
}

Composition 源码分析

现在回到主题,继续看 Koa。

Koa 中 compose_es7 依赖于另一个库完成的: composition,主要的任务是借助 compose 的思想将多个函数组合成一个,废话嘛,当然这不是该库存在的意义,它的真正价值在于对异步(比如Generator, Async/Await,Promise)的支持。。 代码量也不多,一共151行代码,它的执行结果最终返回的是既支持 Promise 也支持 Generator 的对象,这个在 koa 的源码中也可以看出来。

实现看代码,这里只看 Promise 的处理方式,其他的类似。通过结合使用例子来看源码,先例子。

// compose 例子
var compose = require('composition');
var stack = [];

stack.push(function (next) {
  console.log('first');
  return next.then(function(){
    console.log('first is ok');
  })
});

stack.push(function () {
  console.log('second');
  return new Promise(function(resolve){
    setTimeout(function(){
      console.log('second is ok');
      resolve()
    });
  });
});

// 被 composition 组装之后的样子,它的**执行结果**为一个 promise。
compose(stack)().then(function (val) {
  console.log(' ---- final -----');
});

再源码。

// 对外exports 出去的接口
function compose(middleware) {
  return function (next) {
    next = next || new Wrap(noop);
    var i = middleware.length;
    // 注意:这里的 next,作为参数传入到Wrap 中去。
    while (i--) next = new Wrap(middleware[i], this, next);
    // 最终返回的是 **第一个** 被包装过的对象。
    return next
  }
}

// constructor,then, 和 _getPromise() 都没啥可说的。
function Wrap(fn, ctx, next) {
  if (typeof fn !== 'function') throw TypeError('Not a function!');
  this._fn = fn;
  this._ctx = ctx;
  this._next = next;
  this._called = false;
  this._value = undefined;
  this._promise = undefined;
  this._generator = undefined;
}

Wrap.prototype.then = function (resolve, reject) {
  return this._getPromise().then(resolve, reject);
}

Wrap.prototype._getPromise = function () {
  if (this._promise === undefined) {
    var value = this._getValue();
    this._promise = isGenerator(value)
      ? co.call(this._ctx, value)
      : Promise.resolve(value);
  }
  return this._promise
}

// 注意:真正的执行最初压入 stack 的函数是在这里执行的;其次,将 next 当做参数传入到所要执行的 fn 中,记不记得 Koa 中使用的时候,总是 `yield next`,next 就是在这里作为参数传入的。
Wrap.prototype._getValue = function () {
  if (!this._called) {
    this._called = true;
    try {
      this._value = this._fn.call(this._ctx, this._next);
    } catch (e) {
      this._value = Promise.reject(e);
    }
  }
  return this._value
};

对 Generator 的处理大同小异吧,这里略过。正是因为源码中多了 Promise 和 Generator 的互转,所以完成了同时满足这两种异步编程风格的使命。

co 是如何实现的

细心的人可能会发现,Composition 源码部分_getPromise也有用到co,承担了把 Generator 转化为 Promise 的功能。那在 koa 中呢?

还是看这行代码

  var fn = this.experimental
    ? compose_es7(this.middleware)
    : co.wrap(compose(this.middleware));

那就从 co.wrap(compose(this.middleware)) 开始看起吧。co.wrap 可以将一个 generator 形式转化为 promise。在这里先对 middleware 做了组合处理,翻翻 compose 的源码。

function compose(middleware){
  return function* (next){
    var i = middleware.length;
    var prev = next || noop();
    var curr;

    // 通过循环展开中间件
    while (i--) {
      // 获取当前索引下的 generator 并调用该 generator(不是真正的执行),同时为它准备 next 参数。next 参数的值就是队列中当前 i+1 的Iterator Object。
      curr = middleware[i];
      prev = curr.call(this, prev);
    }

    // 最终返回的,便是在队列首部并且已经准备就绪的 Iterator Object。
    yield* prev;
  }
}

单独看这段代码,还不能充分理解,结合一段示例代码来充分消化 compose 的妙处。

var stack = [];
stack.push(function* (next){
  console.log('first-before');
  // 注意:这里使用的是 yield*,不是 yield。

  yield* next;
  console.log('first-after');
});

stack.push(function* (next){
  console.log('second-before');
  yield* next;
  console.log('second-after');
});

stack.push(function* (){
  console.log('third');
});

var fn = compose(stack);

for(var ret of fn()) {
  console.log(ret.value);
}

/**
///////////////// 输出
first-before
second-before
third
second-after
first-after
**/

当 fn 开始执行时,会从第一个 Generator 生成的 Iterator Object 开始执行,并带有 next 参数。就像拨洋葱一样,一层一层深入,再一层层出来,回忆一下 Koa 官网的那个配图。

插播关于 yield 和 yield*

compose 的最后一行 yield* prevyield prev 区别是什么呢?从概念上就可以看出两者的区别:

如果需要在 Generator 函数 内部,调用另一个 Generator 函数,则必须使用yield*,否则没有效果。换句话说,yield*等于在外层的 Generator 函数内部,部署了内层 Generator 函数的 for of 循环。

看例子。

function* inner() {
  yield 'inner';
}

function* outer_without_asterisk() {
  yield 'outer - begin';
  yield inner();
  yield 'outer - after';
}

function* outer_with_asterisk() {
  yield 'outer - begin';
  yield* inner();
  yield 'outer - after';
}

for(var val of outer_without_asterisk()) {
  console.log(val);
}
/**
///////// 输出。 并没看到 inner 迭代器的输出,因为 `yield inner() 这里返回的只是 Iterator Object.
outer - begin
{}
outer - after
**/
for(var val of outer_with_asterisk()) {
  console.log(val);
}
/**
///////// 输出。 并没看到 inner 迭代器的输出,因为 `yield inner() 这里返回的只是 Iterator Object.
outer - begin
inner
outer - after
**/

Ok,在回到主线任务前,还有一个问题,还没展开,就是

Koa 官网中的中间件使用的方式还记得吗?

app.use(function* (next) {
  yield next;
});

而在我刚才的示例代码中是

stack.push(function* (next){
  // ...
  yield* next;
  // ...
});

前者使用的是 yield(前者也可以使用 yield_),后者必须使用 yield_ 这是为什么?这个问题当做 谜2 放在 co 的源码分析完之后自会有答案。

现在回头再看,发现compose其实和上面 composition 差不了太多,区别比较大的地方是是这里返回的是 Generator。返回后的 Generator 交给 co.wrap 进一步处理。

// 如上面提到的,只是将 generator 转化为 promise。
co.wrap = function (fn) {
  // 缓存着原始的 generator,以备不时之需。
  createPromise.__generatorFunction__ = fn;
  return createPromise;
  function createPromise() {
    return co.call(this, fn.apply(this, arguments));
  }
};

真正的 co 函数长什么样子,打开这个传颂很久的神秘盒子看看。

function co(gen) {
  var ctx = this;
  var args = slice.call(arguments, 1)

  return new Promise(function(resolve, reject) {
    if (typeof gen === 'function') gen = gen.apply(ctx, args);
    if (!gen || typeof gen.next !== 'function') return resolve(gen);

    // 启动 generator
    onFulfilled();

    function onFulfilled(res) {
      var ret;
      try {
        ret = gen.next(res);
      } catch (e) {
        return reject(e);
      }
      // 将执行结果传入 next
      next(ret);
    }

    // 异常处理代码略掉...

    function next(ret) {
      // 根据 ret 的各种情况进行分别处理。
      // 如果迭代器已经执行完毕,则通过 Promise 的 resove 函数触发改变 Promise 的状态。
      if (ret.done) return resolve(ret.value);

      // 将迭代器上一步的返回值进行 promise 化处理。
      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) + '"'));
    }
  });

上一步中,对迭代器每一步的返回值都进行 toPromise 转变。

// 根据obj 的不同类型进行不同方式的 promise 形式的转变。比如数组时,对象时,或者函数,还有当是 Generator 时。
function toPromise(obj) {
  if (!obj) return obj;
  if (isPromise(obj)) return obj;

  // 当为 generator 时,再调用 co 对该 obj 做一次转化为 promise 的操作。
  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;
}

看到这里是不是有点拨开云雾见天日的感觉呢。在对 obj 做 toPromise 的转变时,如果 obj 为 Generator,那就再 obj 上再调用一次 co,co 函数内部会执行这个 Generator,并通过返回的 promise 跟踪状态,触发相关逻辑(回想上面的 co 函数源码)。对外部的好处,统一了编程风格,自然也就不用再去 care yield 关键字后面到底是个异步任务呢,还是个 Generator 呢。

谜1和谜2 的疑团是不是解开了呢。不过话说回来,多一次的调用,自然就多出一点额外的性能损耗,如果追求极致的性能,那就明确指名使用 yield* next,如果需要统一的风格,那大可以直接用 yield next 的形式。via: promote yield* next instead of yield next

结语

到这里,Koa 的源码基本粗略得过完了,肯定还有一些比较细节但处理比较好的地方尚未顾及到,等再有机会 review 时再做补充吧。

有没有发现什么值得学习借鉴的地方,或者好的思想,有,三点。

  • 代码严谨。废话,但确实是这样。
  • 对中间件的处理。这个可能和最近自己在看 FP 有关,所以对 compose 很感兴趣。整篇文章也用了估计一半的篇幅再说这个。
  • 有没有看到大量的 call(ctx) 或者 call(this) 样子的代码,层层深入,直观看这个 api,没有人不知道,但层层深入的价值是什么呢?即便是到了框架最深层,也依然没有丢失上层传入的 this 指向(在有必要的前提下)。

参考资料

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant