4

最近一年零零散散看了不少开源项目的源码, 多少也有点心得, 这里想通过这篇文章总结一下, 这里以Koa为例, 前段时间其实看过Koa的源码, 但是发现理解的有点偏差, 所以重新过一遍.

不得不说阅读tj的代码真的收获很大, 没啥奇技淫巧, 代码优雅, 设计极好. 注释什么的就更不用说了. 总之还是推荐把他的项目都过一遍(逃)

跑通例子

Koa作为一个web框架, 我们要去阅读它的源码肯定是得知道它的用法, Koa的文档也很简单, 它一开始就提供了一个例子:

const Koa = require('koa');
const app = new Koa();

app.use(async ctx => {
  ctx.body = 'Hello World';
});

app.listen(3000);

这是启动最基本的的web服务, 这个跑起来没啥问题.

同样, 文档也提供了作为Koa的核心卖点的中间件的基本用法:

const Koa = require('koa');
const app = new Koa();

// x-response-time

app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  ctx.set('X-Response-Time', `${ms}ms`);
});

// logger

app.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  console.log(`${ctx.method} ${ctx.url} - ${ms}`);
});

// response

app.use(async ctx => {
  ctx.body = 'Hello World';
});

app.listen(3000);

上面代码可能跟我们之前写的js代码常识不太符合了, 因为async/await会暂停作案现场, 类似同步. 也就是碰到await next, 代码会跳出当前中间件, 执行下一个, 最终还回原路返回, 依次执行await next下面的代码, 当然这只是一个表述而已, 实际就是一个递归返回Promise, 后面会提到.

阅读目标

好了. 我们知道Koa怎么用了, 那对于这个框架我们想知道什么呢. 先看一下源码的目录结构好了:

image

注意这个compose.js是我为了方便修改源码拉过来的, 其实它是额外的一个包.

application.js 作为入口文件肯定是个构造函数
context.js 就是ctx
request.js
response.js

那我们读源码总需要一个目标吧, 这篇文章里我们假定目标就是弄懂Koa的中间件原理好了

分析执行流程

好, 目标也有了, 下面正式进入源码阅读状态. 我们以最简单的示例代码作为入口来切入Koa的执行过程:

const app = new Koa();

上面我们可以看到Koa是作为构造函数引用的, 那么我们来看看入口文件Application.js 导出了个啥:

module.exports = class Application extends Emitter { 
 // ...
}

毫无疑问是可以对应上的, 导出了一个类.

app.use(async ctx => {
  ctx.body = 'Hello World';
});

看上面的东西似乎进入正题了, 我们知道use就是引用了一个中间件, 那来看看use是个啥玩意:

use(fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
    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);
    }
    debug('use %s', fn._name || fn.name || '-');
    this.middleware.push(fn);
    return this;
  }

太长太臭, 精简一下

use(fn) {
    this.middleware.push(fn);
    return this;
  }

emm 这下就很清楚了, 就是维护了一个中间件数组middleware, 到这里不要忘了我们的目标: Koa的中间件原理, 既然找到这个中间件数组了, 我们就来看看它是怎么被调用的吧. 全局搜一下, 我们发现其实就一个方法里用到了middleware:

  callback() {
    const fn = compose(this.middleware);

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

    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn);
    };

    return handleRequest;
  }

上面的代码可以看到, 似乎有一个compose对middleware进行处理了, 我们好像离真相越来越近了

function compose (middleware) {

  /**
   * @param {Object} context
   * @return {Promise}
   * @api public
   */

  return function (context, next) {
    // last called middleware #
    let index = -1
    return dispatch(0)
    function dispatch (i) {
      index = i
      let fn = middleware[i]
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, function next () {
          return dispatch(i + 1)
        }))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

删除边界条件, 错误处理

compose.js的代码很短, 但是还是嫌长怎么办, 之前有文章提到的, 删除边界条件和异常处理:

function compose (middleware) {

  /**
   * @param {Object} context
   * @return {Promise}
   * @api public
   */

  return function (context, next) {
    let index = -1
    return dispatch(0)
    function dispatch (i) {
      index = i
      let fn = middleware[i]
      if (!fn) return Promise.resolve()
      return Promise.resolve(fn(context, function next () {
          return dispatch(i + 1)
        }))
    }
  }
}

这么一看就清晰多了, 不就是一个递归遍历middleware嘛. 似乎跟express有点像.

猜想结论

大胆假设嘛, 前面提到了, await 会暂停执行, 那await next 似乎暂停的就是这里, 然后不断递归调用中间件, 然后递归中断了, 代码又从一个个的promise里退出来, 似乎这样就很洋葱了.

emm 到底是不是这样呢, 我也不知道. 比较还想再水一篇文章呢.

image


xiadd
2.6k 声望88 粉丝