如何阅读源码--Koa为例

最近一年零零散散看了很多开源项目的源码, 多少也有点心得, 这里想经过这篇文章总结一下, 这里以Koa为例, 前段时间其实看过Koa的源码, 可是发现理解的有点误差, 因此从新过一遍.git

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

跑通例子

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

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

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

app.listen(3000);
复制代码

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

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

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怎么用了, 那对于这个框架咱们想知道什么呢. 先看一下源码的目录结构好了:promise

image

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

application.js 做为入口文件确定是个构造函数 context.js 就是ctxrequest.js response.js框架

那咱们读源码总须要一个目标吧, 这篇文章里咱们假定目标就是弄懂Koa的中间件原理好了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
相关文章
相关标签/搜索