Koa2源碼學習(上)

NO IMAGE

引言

最近讀了一下Koa2的源碼;在閱讀Koa2 (2.3.0) 的源碼的過程中,我的感受是整個代碼設計精巧,思路清晰,是一個小而精的 nodejs web服務框架。

設計理念

作為web服務框架,都是要圍繞核心服務而展開的。那什麼是核心服務呢?其實就是接收客戶端的一個http的請求,對於這個請求,除了接收以外,還有解析這個請求。所以說會有

HPPT:接收 -> 解析 -> 響應

在響應客戶端的時候,也有很多種方式,比如返回一個html頁面,或者json文本。在解析請求和響應請求的中間,會有一些第三方的中間件,比如 日誌、表單解析等等來增強 koa 的服務能力,所以 koa 至少要提供 “請求解析”、”響應數據”、”中間件處理” 這三種核心能力的封裝,同時還需要有一個串聯他們執行環境的上下文(context)

  • HTTP
  • 接收
  • 解析
  • 響應
  • 中間件
  • 執行上下文

上下文可以理解為是http的請求週期內的作用域環境來託管請求響應和中間件,方便他們之間互相訪問。

以上分析是站在單個http請求的角度來看一個web服務能力。那麼站在整個網站,站在整個後端服務的角度來看的話,能夠提供 “請求”、”響應”、”解析”、”中間件”、”http流程全鏈路” 這些服務能力的綜合體,可以看做是一個應用服務對象。如果把這些全放到 koa 裡的話,那麼對應的就是:

  • Application
  • Context
  • Request
  • Response
  • Middlewares
  • Session
  • Cookie

Koa的組成結構

首先看下koa的目錄結構

Koa2源碼學習(上)

  • application.js:框架入口;負責管理中間件,以及處理請求
  • context.js:context對象的原型,代理request與response對象上的方法和屬性
  • request.js:request對象的原型,提供請求相關的方法和屬性
  • response.js:response對象的原型,提供響應相關的方法和屬性
// application.js

const isGeneratorFunction = require('is-generator-function'); // 判斷當前傳入的function是否是標準的generator function
const debug = require('debug')('koa:application'); // js調試工具
const onFinished = require('on-finished'); // 事件監聽,當http請求關閉,完成或者出錯的時候調用註冊好的回調
const response = require('./response'); // 響應請求
const compose = require('koa-compose'); // 中間件的函數數組
const isJSON = require('koa-is-json'); // 判斷是否為json數據
const context = require('./context'); // 運行服務上下文
const request = require('./request'); // 客戶端的請求
const statuses = require('statuses'); // 請求狀態碼 
const Cookies = require('cookies');
const accepts = require('accepts'); // 約定可被服務端接收的數據,主要是協議和資源的控制
const Emitter = require('events'); // 事件循環
const assert = require('assert'); // 斷言
const Stream = require('stream');
const http = require('http');
const only = require('only'); // 白名單選擇
const convert = require('koa-convert'); // 兼容舊版本koa中間件
const deprecate = require('depd')('koa'); // 判斷當前在運行koa的某些接口或者方法是否過期,如果過期,會給出一個升級的提示

以上是koa入口文件的依賴分析。接下來我們進行源碼分析,首先我們利用刪減法來篩出代碼的核心實現即可,不用上來就盯細節!
我們只保留constructor

// application.js

module.exports = class Application extends Emitter {
  constructor() {
    super();

    this.proxy = false; // 是否信任 proxy header 參數,默認為 false
    this.middleware = []; //保存通過app.use(middleware)註冊的中間件
    this.subdomainOffset = 2; // 子域默認偏移量,默認為 2
    this.env = process.env.NODE_ENV || 'development'; // 環境參數,默認為 NODE_ENV 或 ‘development’
    this.context = Object.create(context); //context模塊,通過context.js創建
    this.request = Object.create(request); //request模塊,通過request.js創建
    this.response = Object.create(response); //response模塊,通過response.js創建
  }

  // ...
}

我們可以看到,這段代碼暴露出一個類,構造函數內預先聲明瞭一些屬性,該類繼承了Emitter,也就是說這個類可以直接為自定義事件註冊回調函數和觸發事件,同時可以捕捉到其他地方觸發的事件。

除了這些基本屬性之外,還有一些公用的api,最重要的兩個一個是==listen==,一個是==use==。koa的每個實例上都會有這些屬性和方法。

// application.js

module.exports = class Application extends Emitter {
  constructor() {
    super();

    this.proxy = false;
    this.middleware = [];
    this.subdomainOffset = 2;
    this.env = process.env.NODE_ENV || 'development';
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);
  }

  listen() {
    const server = http.createServer(this.callback());
    return server.listen.apply(server, arguments);
  }

  use(fn) {
    this.middleware.push(fn);
    return this;
  }
}

listen 方法內部通過 http.createServer 創建了一個http服務的實例,通過這個實例去 listen 要監聽的端口號,http.createServer 的參數傳入了 this.callback 回調

// application.js

module.exports = class Application extends Emitter {
  ...
  callback() {
    const fn = compose(this.middleware); // 把所有middleware進行了組合,使用了koa-compose

    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn); // 返回了本身的回調函數
    };

    return handleRequest;
  }
}

可以看到,handleRequest 返回了本身的回調,接下來看 handleRequest 。

handleRequest 方法直接作為監聽成功的調用方法。已經拿到了 包含 req res 的 ctx 和可以執行所有中間件函數的 fn。
首先一進來默認設置狀態碼為==404== . 然後分別聲明瞭 成功函數執行完成以後的成功 失敗回調方法。這兩個方法實際上就是再將 ctx 分化成 req res。 分別調這兩個對象去客戶端執行內容返回。
==context.js request.js response.js== 分別是封裝了一些對 ctx req res 操作相關的屬性,我們以後再說。

// application.js

module.exports = class Application extends Emitter {
  ...
  handleRequest(ctx, fnMiddleware) {
    const res = ctx.res; // 拿到context.res
    res.statusCode = 404; // 設置默認狀態嗎404
    const onerror = err => ctx.onerror(err); // 設置onerror觸發事件
    const handleResponse = () => respond(ctx); // 向客戶端返回數據
    onFinished(res, onerror);
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }
}

失敗執行的回調

onerror(err) {
  assert(err instanceof Error, `non-error thrown: ${err}`);

  if (404 == err.status || err.expose) return;
  if (this.silent) return;

  const msg = err.stack || err.toString();
  console.error();
  console.error(msg.replace(/^/gm, '  '));
  console.error();
}

成功執行的回調

function respond(ctx) {
  ...
}

return fnMiddleware(ctx).then(handleResponse).catch(onerror); 我們拆分理解,首先 return fnMiddleware(ctx) 返回了一箇中間件數組處理鏈路,then(handleResponse) 等到整個中間件數組全部完成之後把返回結果通過 then 傳遞到 handleResponse。

// application.js

module.exports = class Application extends Emitter {
  ...
  createContext(req, res) {
    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.cookies = new Cookies(req, res, {
      keys: this.keys,
      secure: request.secure
    });
    request.ip = request.ips[0] || req.socket.remoteAddress || '';
    context.accept = request.accept = accepts(req);
    context.state = {};
    return context;
  }
}

這裡我們不用去太深入去摳代碼,理解原理就行。createContext 創建 context 的時候,還會將 req 和 res 分別掛載到context 對象上,並對req 上一些關鍵的屬性進行處理和簡化 掛載到該對象本身,簡化了對這些屬性的調用。我們通過一張圖來直觀地看到所有這些對象之間的關係。

Koa2源碼學習(上)

  • 最左邊一列表示每個文件的導出對象
  • 中間一列表示每個Koa應用及其維護的屬性
  • 右邊兩列表示對應每個請求所維護的一些列對象
  • 黑色的線表示實例化
  • 紅色的線表示原型鏈
  • 藍色的線表示屬性

createContext 簡單理解就是掛載上面的對象,方便整個上下游http能及時訪問到進出請求及特定的行為。

// application.js

module.exports = class Application extends Emitter {
  ...
}
function respond(ctx) {
  // allow bypassing koa
  if (false === ctx.respond) return;

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

  let body = ctx.body;
  const code = ctx.status; // 賦值服務狀態碼

  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);
  }

  // 通過判斷body類型來調用,這裡的res.end就是最終向客戶端返回數據的動作
  if (Buffer.isBuffer(body)) return res.end(body);
  if ('string' == typeof body) return res.end(body);
  if (body instanceof Stream) return body.pipe(res);

  // 返回為json數據
  body = JSON.stringify(body);
  if (!res.headersSent) {
    ctx.length = Buffer.byteLength(body);
  }
  res.end(body);
}

respond 函數是 handleRequest 成功處理的回調,內部做了合理性校驗,諸如狀態碼,內容的類型判斷,最後向客戶端返回數據。

結語

以上就是我們對application.js文件的分析,通過上面的分析,我們已經可以大概得知Koa處理請求的過程:當請求到來的時候,會通過 req 和 res 來創建一個 context (ctx) ,然後執行中間件。

相關文章

struts結果類型

CSS攻擊:記錄用戶密碼

Redux梳理分析【一:reducer和dispatch】

Babel插件起手式