Koa2 源碼學習(上)

引言

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

設計理念

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

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

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

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

上下文能夠理解爲是http的請求週期內的做用域環境來託管請求響應和中間件,方便他們之間互相訪問。後端

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

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

Koa的組成結構

首先看下koa的目錄結構數組

  • 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入口文件的依賴分析。接下來咱們進行源碼分析,首先咱們利用刪減法來篩出代碼的核心實現便可,不用上來就盯細節! 咱們只保留constructorbash

// 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,也就是說這個類能夠直接爲自定義事件註冊回調函數和觸發事件,同時能夠捕捉到其餘地方觸發的事件。cookie

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

// 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 上一些關鍵的屬性進行處理和簡化 掛載到該對象自己,簡化了對這些屬性的調用。咱們經過一張圖來直觀地看到全部這些對象之間的關係。

  • 最左邊一列表示每一個文件的導出對象
  • 中間一列表示每一個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) ,而後執行中間件。

相關文章
相關標籤/搜索