资讯专栏INFORMATION COLUMN

如何阅读源码--Koa为例

zhoutk / 537人阅读

摘要:最近一年零零散散看了不少开源项目的源码多少也有点心得这里想通过这篇文章总结一下这里以为例前段时间其实看过的源码但是发现理解的有点偏差所以重新过一遍不得不说阅读的代码真的收获很大没啥奇技淫巧代码优雅设计极好注释什么的就更不用说了总之还是推荐把

最近一年零零散散看了不少开源项目的源码, 多少也有点心得, 这里想通过这篇文章总结一下, 这里以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怎么用了, 那对于这个框架我们想知道什么呢. 先看一下源码的目录结构好了:

注意这个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 到底是不是这样呢, 我也不知道. 比较还想再水一篇文章呢.

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/92602.html

相关文章

  • Koa源码阅读笔记(3) -- 服务器の启动与请求处理

    摘要:本笔记共四篇源码阅读笔记源码阅读笔记源码阅读笔记服务器启动与请求处理源码阅读笔记对象起因前两天阅读了的基础,和中间件的基础。的前端乐园原文链接源码阅读笔记服务器启动与请求处理 本笔记共四篇Koa源码阅读笔记(1) -- coKoa源码阅读笔记(2) -- composeKoa源码阅读笔记(3) -- 服务器の启动与请求处理Koa源码阅读笔记(4) -- ctx对象 起因 前两天阅读了K...

    mrcode 评论0 收藏0
  • Koa源码阅读笔记(4) -- ctx对象

    摘要:本笔记共四篇源码阅读笔记源码阅读笔记源码阅读笔记服务器启动与请求处理源码阅读笔记对象起因前两天终于把自己一直想读的源代码读了一遍。首先放上关键的源代码在上一篇源码阅读笔记服务器启动与请求处理中,我们已经分析了的作用。 本笔记共四篇Koa源码阅读笔记(1) -- coKoa源码阅读笔记(2) -- composeKoa源码阅读笔记(3) -- 服务器の启动与请求处理Koa源码阅读笔记(4...

    ityouknow 评论0 收藏0
  • Koa源码阅读笔记(2) -- compose

    摘要:于是抱着知其然也要知其所以然的想法,开始阅读的源代码。问题读源代码时,自然是带着诸多问题的。源代码如下在被处理完后,每当有新请求,便会调用,去处理请求。接下来会继续写一些阅读笔记,因为看的源代码确实是获益匪浅。 本笔记共四篇Koa源码阅读笔记(1) -- coKoa源码阅读笔记(2) -- composeKoa源码阅读笔记(3) -- 服务器の启动与请求处理Koa源码阅读笔记(4) -...

    roland_reed 评论0 收藏0
  • B站Up主-山地人-这位老哥2019年的前端自学计划进展如何?——讲一个B站Up主自学前端85天的故

    摘要:前言自从上次在掘金发布年山地人的前端完整自学计划讲一个站主山地人的天前端自学故事以来,一眨眼山地人老哥在站做主已经有天了。所以这个体系里的一些框架包括也是山地人年自学计划的一部分。月底,山地人老哥开启了的两个专题。 前言 自从上次在掘金发布【2019年山地人的前端完整自学计划——讲一个B站UP主山地人的40天前端自学故事】 以来,一眨眼山地人老哥在B站做Up主已经有85天了。 时隔一个...

    cocopeak 评论0 收藏0
  • Koa源码阅读笔记(1) -- co

    摘要:正好自己之前也想看的源代码,所以趁着这个机会,一口气将其读完。源码解读的源代码十分简洁,一共才两百余行。结语的源代码读取来不难,但其处理方式却令人赞叹。而且阅读的源代码,是阅读源码的必经之路。 本笔记共四篇Koa源码阅读笔记(1) -- coKoa源码阅读笔记(2) -- composeKoa源码阅读笔记(3) -- 服务器の启动与请求处理Koa源码阅读笔记(4) -- ctx对象 起...

    taoszu 评论0 收藏0

发表评论

0条评论

zhoutk

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<