如何閱讀原始碼–Koa為例

_xiadd_發表於2019-03-03

最近一年零零散散看了不少開源專案的原始碼, 多少也有點心得, 這裡想通過這篇文章總結一下, 這裡以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

相關文章