高質量 - Koa 源碼解析

Koa 源碼解析

一個簡單的 koa 程序

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

// response
app.use(ctx => {
  ctx.body = 'Hello Koa';
});

app.listen(3000);

源碼解析- koa 源碼目錄

  • application.js: koa 的導出文件夾,也就是定義 koa 的地方
  • context.js: 上下文,也就是常見的 ctx。每個 app 都會有一份 context,經過繼承這個 context.js 中定義的 context。同時每個請求中,都會有一份單獨的 ctx, 經過繼承 app 中定義的 context。說白了就是複用。
  • request.js: 同前面的 context.js 說明。
  • response.js: 同前面的 context.js 說明。

說明:在 koanodejs 原生對應的是 reqresnode

源碼解析- application.js


包的功能能夠參考截圖中給出的註釋json

Application


這個就是 koa 的定義的類api

第一步 - new koa()

New 會執行構造函數。

因此在實例化的時候,能夠傳入這些 options數組

第二步 - app.use


在這裏會檢查 middleware 的類型,若是是老的 middleware 會轉換一下,最後直接放到 middleware 這個數組中。數組中的中間件,會在每個請求中去挨個執行一遍。cookie

第三步 - app.listen

listen 的時候,纔會去建立 server

對於每個請求,都會走到 callback 中去,因此 callback 是用於處理實際請求的。通常不要去重寫這個 callbackapp

接下來去看看 callback 作了什麼:

這裏涉及到幾個大的點:dom

  1. createContext 都幹了什麼
  2. Compose 是如何實現洋蔥模型的。
  3. this.handleRequest(ctx, fn) 幹了什麼

這幾個點分紅兩個大塊來說,二、3 兩點放到一塊兒講。koa

createContext 幹了什麼


這裏作了三件重要的事情socket

  1. 每個 app 都有其對應的 context、request、response 實例,每個請求,都會基於這些實例去建立本身的實例。在這裏就是建立了 context、request、response
  2. node 原生的 res、req 以及 this 掛載到 context、request、response 上面。還有一些其餘爲了方便訪問作得一些掛載,不過前面三個的掛載是必須的。
  3. 將建立的 context 返回,傳給全部中間件的第一個 ctx 參數,做爲這個請求的上下文

下面着重解釋一下第二點中,爲何要把這些屬性掛載上去。由於全部的訪問都是代理,最終都是訪問的 req、res 上面的東西,context 訪問的是 request、response 上面的東西,可是他們上面的東西又是訪問的是 req、res 上面的。

例如訪問 ctx.methodcontext 會去 request 上面早,而後 request 會返回 req.method。後面分析其餘文件時會講到這種代理結構。ide

compose 如何實現的洋蔥模型


在第三步中最後講到的 callback 中,middleware 所有經過 koa-compose 這個包包裝,返回了一個可執行的方法,在請求階段會去執行這個方法,從而執行每個中間件。先本身來手擼一個 compose 的🌰

function compose(middleware) {
    return function (ctx, next) {
        function dispatch(i) {
            if (i >= middleware.length) {
                return Promise.resolve()
            }
            let curMiddleware = middleware[i]
            return curMiddleware(ctx, dispatch.bind(null, i + 1))
        }
        return dispatch(0)
    }
}

function mid1(ctx, next) {
    console.log('mid1 before')
    next()
    console.log('mid1 after')
}
function mid2(ctx, next) {
    console.log('mid2 before')
    next()
    console.log('mid2 after')
}
function mid3(ctx, next) {
    console.log('mid3 before')
    next()
    console.log('mid3 after')
}

const fn = compose([mid1, mid2, mid3])
fn({})
--------------------------------------------------------------------打印結果
mid1 before
mid2 before
mid3 before
mid3 after
mid2 after
mid1 after

compose 中會根據 i 去挨個執行中間件,而且有一個回溯的過程。官方代碼以下。

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!')
  }

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

因此總結一下,洋蔥模型本質是經過遞歸去實現的。

講了 compose 的原理以後,回到第三步中最後的 this.handleRequest(ctx, fn); fn 就是compose 返回的包裝過 middleware 的函數。下面進入 handleRequest

能夠看到當一個請求來到的時候,最後會去執行包裝過的中間件函數,也就是這裏的最後一行,並在中間件執行完畢以後,到 handleResponse 中去處理響應。在 handleResponse 中最終執行的是 respond

respond

function respond(ctx) {
  // allow bypassing koa
  if (false === ctx.respond) return;

  if (!ctx.writable) return;

  const res = ctx.res;
  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 && !ctx.response.has('Content-Length')) {
      const { length } = ctx.response;
      if (Number.isInteger(length)) ctx.length = length;
    }
    return res.end();
  }

  // status body
  if (null == body) {
    if (ctx.req.httpVersionMajor >= 2) {
      body = String(code);
    } else {
      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);
}

主要是將 ctx 上掛載的 body 經過 res.end 返回響應。

源碼解析 - request.js

module.exports = {
   /**
   * Return request header.
   *
   * @return {Object}
   * @api public
   */
  get header() {
    return this.req.headers;
  },

  /**
   * Set request header.
   *
   * @api public
   */

  set header(val) {
    this.req.headers = val;
  },
 ..............
}

可見前面在 createContext 的時候在 request 上面去掛載 req、res 的緣由就在這裏。

源碼解析 - response.js

module.exports = {
    /**
   * Return the request socket.
   *
   * @return {Connection}
   * @api public
   */

  get socket() {
    return this.res.socket;
  },
    /**
   * Get response status code.
   *
   * @return {Number}
   * @api public
   */
  get status() {
    return this.res.statusCode;
  },
.......................
}

源碼解析 - context.js

const proto = module.exports = {
.............
  get cookies() {
    if (!this[COOKIES]) {
      this[COOKIES] = new Cookies(this.req, this.res, {
        keys: this.app.keys,
        secure: this.request.secure
      });
    }
    return this[COOKIES];
  },

  set cookies(_cookies) {
    this[COOKIES] = _cookies;
  }
............
}

/**
 * Response delegation.
 */

delegate(proto, 'response')
  .method('attachment')
  .method('redirect')
  .method('remove')
  .method('vary')
  .method('has')
  .method('set')
  .method('append')
  .method('flushHeaders')
  .access('status')
  .access('message')
  .access('body')
  .access('length')
  .access('type')
  .access('lastModified')
  .access('etag')
  .getter('headerSent')
  .getter('writable');

/**
 * Request delegation.
 */

delegate(proto, 'request')
  .method('acceptsLanguages')
  .method('acceptsEncodings')
  .method('acceptsCharsets')
  .method('accepts')
  .method('get')
  .method('is')
  .access('querystring')
  .access('idempotent')
  .access('socket')
  .access('search')
  .access('method')
  .access('query')
  .access('path')
  .access('url')
  .access('accept')
  .getter('origin')
  .getter('href')
  .getter('subdomains')
  .getter('protocol')
  .getter('host')
  .getter('hostname')
  .getter('URL')
  .getter('header')
  .getter('headers')
  .getter('secure')
  .getter('stale')
  .getter('fresh')
  .getter('ips')
  .getter('ip');

這裏的 proto 就是 context,在自身定義了一個經常使用的方法,可經過 ctx.method 去訪問,還有後面使用 delegate ,這個函數會把自 context 上面的 request、response 上面的一些屬性定義到 proto 也就是 context 上面去,可是當使用 ctx.xxx 去訪問的時候,實際上是訪問 request、response 上面的屬性,這也是爲何須要將 request、response 掛載到 context 上面去。

相關文章
相關標籤/搜索