Koa 原始碼閱讀筆記

Wendell_Hu發表於2018-09-30

這篇文章介紹一個應用伺服器框架的主要兩個過程: app init 過程和 request handle 過程. 一些有趣的細節問題看看以後再寫, 包括 context, request, response 三個物件, 錯誤處理, egg.js 等等.

這是我讀的第二個框架, 它比 Flask 簡單多了...

init 過程

通過一個簡單的 demo (實際上就是官網的例子) 來講解 app init 過程. 對於 Koa 來說, init 過程是比較簡單的.

const Koa = require('koa')
const app = new Koa() // Koa 物件例項化

// use 增加 middleware
app.use(async (ctx, next) => {
  await next();
  const rt = ctx.response.get('X-Response-Time')
  console.log(`${ctx.method} ${ctx.url} - ${rt}`)
});

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

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

app.listen(3000) // 監聽埠
複製程式碼

Koa 物件例項化

lib/application.js

  constructor() {
    super();

    this.proxy = false;
    this.middleware = [];
    this.subdomainOffset = 2;
    this.env = process.env.NODE_ENV || 'development';

    // 在應用程式例項上繫結 context request repsonse 的原型, 實際上這三個物件都沒有任何屬性
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);

    if (util.inspect.custom) {
      this[util.inspect.custom] = this.inspect;
    }
  }
複製程式碼

use 增加 middleware

  use(fn) {
    if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');

    // 如果是一個生成器函式要轉換成 async 函式, 細節問題
    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 當中
    this.middleware.push(fn);
    return this; // 通過返回自己可以進行鏈式呼叫
  }
複製程式碼

監聽埠

  listen(...args) {
    debug('listen');
    const server = http.createServer(this.callback());
    return server.listen(...args); // 呼叫 Node.js 原生的方法監聽埠
  }
複製程式碼

this.callback() 方法返回一個回撥函式, 它符合 Node.js 原生 http.createServer 的要求, 被當作 request handler.

  callback() {
    // 將自己繫結的中介軟體封裝起來
    const fn = compose(this.middleware);

    // 進行錯誤處理的回撥函式
    // 由於 Koa 繼承了 Emitter, 所以使用者可以在上面繫結 error 方法, 如果使用者沒用繫結, 就繫結自帶的 onerror 方法
    // 錯誤處理暫時不講
    if (!this.listenerCount('error')) this.on('error', this.onerror);

    // http server 的回撥函式
    const handleRequest = (req, res) => {
      // 將 request response 物件封裝為 context 物件, 然後開始對 request 的處理過程, 這個放到第二節再講
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn); // 返回對 request 的處理結果
    };

    return handleRequest;
  }
複製程式碼

compose

這是個很重要的方法, 其返回的 fn, 將會在請求到達的時候實際負責 context 在 middleware 中的傳遞.

function compose (middleware) {
  // 型別檢查
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }

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

  // 這個函式簽名就是 koa middleware 常見的函式簽名
  // 它作為 this.handleRequest 的引數, this.handleRequest 會呼叫它
  // 注意! 下面的程式碼及註釋請在閱讀 request handler 的過程閱讀
  return function (context, next) {
    // last called middleware #
    // 指示 context 在 middleware 鏈上的位置
    // context 剛來的時候沒有進入鏈, 所以 index === -1
    let index = -1

    // 從第 1 個 middleware 開始 context 之旅, index === 0
    return dispatch(0)

    // 這個 dispatch 串接 context 在 middleware 中的流動
    function dispatch (i) {
      // 如果 i 到了起點之前, 說明 next 被用了太多次
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i // 壓棧階段, 記錄自己所在的位置
      let fn = middleware[i]
      if (i === middleware.length) fn = next // 如果走到了 middleware 的最後一站, 那麼就用傳入的 next 當作 next
      if (!fn) return Promise.resolve() // 如果都沒有 middleware, 直接返回, 然後層層 resolve 返回
      try {
        // 進入 middleware 函式的執行過程, middleware 中訪問的 next 被定義在這裡, 
        // 而當這個 middleware 呼叫 next 的時候, 就等於呼叫 dispatch, 同時進入 middleware 的下一層
        // 如果當前 middleware 是最後一個, 上面的 if (i === middleware.length) fn = next 邏輯就會被啟用, 頂層呼叫 next
        // 可以看到我們 await 的東西就是一個 resolved 的 Promise!
        // 根據 async 函式的定義, 預設返回的就是一個 resolved 的 Promise<undefined>
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        // 如果丟擲了異常, 就會被捕獲, 層層 reject 回來
        return Promise.reject(err)
      }
    }
  }
}
複製程式碼

request handle 過程

還是用上面的例子來講解 request handle 過程.

當有 http 請求過來的時候, 如下的方法最先被呼叫:

  const handleRequest = (req, res) => {
    const ctx = this.createContext(req, res); // 建立 context
    return this.handleRequest(ctx, fn); // 過程處理 === context 在 middleware 中的傳遞
  };
複製程式碼

建立 context

  createContext(req, res) {
    // 建立三個物件, 將它們的 prototype 分別指向 this.context, this.request, this.repsonse, 實際上這三個物件 hasOwnProperties 為空
    // 然後就是各種引用, 比較簡單
    const context = Object.create(this.context);
    const request = context.request = Object.create(this.request);
    const response = context.response = Object.create(this.response);
    context.app = request.app = response.app = this;
    context.req = request.req = response.req = req;
    context.res = request.res = response.res = res;
    request.ctx = response.ctx = context;
    request.response = response;
    response.request = request;
    context.originalUrl = request.originalUrl = req.url;
    context.state = {};
    return context;
  }
複製程式碼

處理過程

對 request 的實際處理過程.

  handleRequest(ctx, fnMiddleware) {
    // 這裡的 fnMiddleware 即是 compose() 返回的 fn 的函式, 可以看到並沒有給第二引數傳遞值, 所以在那裡 next === undefined, 直接 resolve
    const res = ctx.res;
    res.statusCode = 404;

    // 準備兩個 Promise 的回撥函式
    const onerror = err => ctx.onerror(err);
    const handleResponse = () => respond(ctx);
    onFinished(res, onerror);
    
    // 開始 middleware 的傳遞過程
    // 如果執行過程成功, 並沒有從 Promise 裡拿任何的引數, 是利用閉包訪問的 ctx 來生成響應的
    // 但執行失敗則要從 Promise 鏈條裡拿到錯誤資訊
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }
複製程式碼

context 在 middleware 中間的傳遞

fnMiddleware 被呼叫的時候, 即這個函式被呼叫:

function (context, next) {
  // last called middleware #
  // 指示 context 在 middleware 鏈上的位置
  // context 剛來的時候沒有進入鏈, 所以 index === -1
  let index = -1

  // 從第 1 個 middleware 開始 context 之旅, index === 0
  return dispatch(0)

  // 這個 dispatch 串接 context 在 middleware 中的流動
  // 注意! 下面的程式碼及註釋在閱讀 request handler 的過程閱讀
  function dispatch (i) {
    // 如果 i 到了起點之前, 說明 next 被用了太多次
    if (i <= index) return Promise.reject(new Error('next() called multiple times'))
    index = i // 壓棧階段, 記錄自己所在的位置
    let fn = middleware[i]
    if (i === middleware.length) fn = next // 如果走到了 middleware 的最後一站, 那麼就用傳入的 next 當作 next
    if (!fn) return Promise.resolve() // 如果都沒有 middleware, 直接返回, 然後層層 resolve 返回
    try {
      // 進入 middleware 函式的執行過程, middleware 中訪問的 next 被定義在這裡, 
      // 而當這個 middleware 呼叫 next 的時候, 就等於呼叫 dispatch, 同時進入 middleware 的下一層
      // 如果當前 middleware 是最後一個, 上面的 if (i === middleware.length) fn = next 邏輯就會被啟用, 頂層呼叫 next
      // 可以看到我們 await 的東西就是一個 resolved 的 Promise!
      // 根據 async 函式的定義, 預設返回的就是一個 resolved 的 Promise<undefined>
      return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
    } catch (err) {
      // 如果丟擲了異常, 就會被捕獲, 層層 reject 回來
      return Promise.reject(err)
    }
  }
}
複製程式碼

可以看到這個函式是遞迴的:

用我們的例子:

  1. 執行 dispatch(0), 我們註冊的第一個非同步函式被當成 fn, 然後 fn(context, dispatch.bind(null, 1)) 呼叫了這第一個非同步函式
  2. 第一個非同步函式執行 next(), 實際上執行了 dispatch(1), 然後呼叫了第二個非同步函式...
  3. 同理, 呼叫了第三個非同步函式, middleware 到這裡已經全部執行過了
  4. 第三個函式執行的時候沒用再呼叫 next(), 所以非同步函式返回了狀態為 resolved 的 Promise<undefined>
  5. return Promise.resolve() 把非同步函式返回的 Promise 接著 resolved 下去
  6. 直到 dispatch(0) 中的 resolved 的 Promise 被 return 出去
  7. handleRequest 進入 fnMiddleware(ctx).then(handleResponse), 執行 handleResponse
例外情形
如果最後一箇中介軟體也呼叫了 next

此時 fn === undefined, 並且 next === undefined, 所以就會直接返回已 resolved 的 Promise, 開始回溯.

如果有一箇中介軟體呼叫了兩次 next

我們已經知道每次呼叫 next 實際是呼叫了一次 dispatch(i), 如果我們呼叫了同一個 next 兩次, 那麼第二次呼叫的時候, i === index 的條件就會成立. 我們說過 index 是指示 context 在 middleware 中的位置的.

建立響應

function respond(ctx) {
  // allow bypassing koa
  // 允許 bypass 直通 koa
  if (false === ctx.respond) return;

  const res = ctx.res;
  if (!ctx.writable) return;

  let body = ctx.body;
  const code = ctx.status;

  // ignore body
  if (statuses.empty[code]) {
    // strip headers
    ctx.body = null;
    return res.end();
  }

  if ('HEAD' == ctx.method) {
    if (!res.headersSent && isJSON(body)) {
      ctx.length = Buffer.byteLength(JSON.stringify(body));
    }
    return res.end();
  }

  // status body
  if (null == body) {
    body = ctx.message || String(code);
    if (!res.headersSent) {
      ctx.type = 'text';
      ctx.length = Buffer.byteLength(body);
    }
    return res.end(body);
  }

  // responses
  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
  body = JSON.stringify(body);
  if (!res.headersSent) {
    ctx.length = Buffer.byteLength(body);
  }
  res.end(body);
}
複製程式碼

這個方法和 Koa 的關係不大了. 其實就是在處理 response 的各種可能情況, 然後呼叫 http 模組 res 的方法返回響應.

相關文章