koa2原始碼解析

julyL發表於2018-08-01

koa2原始碼解析

koa版本2.4.1

執行流程

以如下示例程式碼進行說明

const Koa = require("koa");

// 1.執行建構函式
const app = new Koa();

// 2.註冊中介軟體
app.use(async (ctx, next) => {
  ctx.body = "Hello World";
});

// 3.啟動指定埠的http服務
app.listen(3000);
複製程式碼

1.建構函式

  constructor() {
    super();                                              // 繼承至Emitter

    this.proxy = false;                                   // 是否設定代理
    this.middleware = [];                                 // 儲存app.use註冊的中介軟體
    this.subdomainOffset = 2;     
    this.env = process.env.NODE_ENV || "development";     // 環境變數

    this.context = Object.create(context);                // this.context物件之後會新增屬性擴充套件成ctx物件
    this.request = Object.create(request);                
    this.response = Object.create(response);
    // context,request,response物件詳細說明見context.js,request.js,response.js
  }
複製程式碼

2.註冊中介軟體

app.use(fn)主要就是將fn放入中介軟體陣列中,並通過返回this實現鏈式呼叫

use(fn){
    // 省略轉換function*的邏輯
    this.middleware.push(fn);   
    return this;               
}
複製程式碼

3.啟動指定埠的http服務

從如下程式碼可以看出app.listen內部還是呼叫原生的http模組來啟動服務

 listen(...args) {
    const server = http.createServer(this.callback()); // 呼叫原生http.createServer啟動服務
    return server.listen(...args);
 }
複製程式碼

this.callback執行會返回handleRequest作為http.createServer的引數

  callback() {
    // 處理中介軟體, 實現洋蔥模型的核心方法
    const fn = compose(this.middleware);

    // 沒有監聽error事件則繫結預設error事件處理
    if (!this.listeners("error").length) this.on("error", this.onerror);

    // http.createServer(handleRequest)
    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res);   // 對原生的req,res進行擴充套件封裝成ctx物件
      return this.handleRequest(ctx, fn);         // 處理請求(執行中介軟體並設定res物件)
    };

    return handleRequest;
  }
複製程式碼

callback方法是koa對中介軟體處理以及設定響應的核心邏輯
我們先來看下compose(this.middleware), compose實現了koa中介軟體呼叫邏輯

// koa-compose模組
function compose(middleware){
    return function (context, next) {
    let index = -1;
    return dispatch(0);    // 返回一個函式,用於開始執行第一個中介軟體,可以通過執行next呼叫後續中介軟體

    // dispatch會始終返回一個Promise物件,koa中介軟體的非同步處理邏輯核心就是利用Promise鏈
    function dispatch(i) {
      if (i <= index) {
        // 變數index由於js閉包會在中介軟體執行過程中一直存在,用於判斷next是否多次執行
        return Promise.reject(new Error("next() called multiple times"));
      }
      index = i;
      let fn = middleware[i];

      // 如果所有的中介軟體都已執行完,由於koa執行compose返回的函式fnMiddleware(ctx)並沒有傳next,所以fn為undefined,直接返回Promise.resolve()
      if (i === middleware.length) fn = next;
      if (!fn) return Promise.resolve();

      try {
        /* 
        當前中介軟體被包裹成了Promise物件,並且next中通過dispatch(i+1)來執行下一個中介軟體。需要注意一點next中必須return。因為Promise執行機制是:當promise1物件return另一個pormise2,只有pomrise2狀態變為resolved之後,promise1才會resolved。如果沒有return一個Promise,那麼當前中介軟體執行完之後這個Promise就resolved,後續中介軟體可能就不會執行
        */
        return Promise.resolve(
          fn(context, function next() {
            return dispatch(i + 1);
          })
        );
      } catch (err) {
        // 中介軟體執行發生異常時,直接rejected停止後續中介軟體的執行。只需要在最後返回的Promise新增catch,就可以捕獲已經執行過的中介軟體發生異常
        return Promise.reject(err);
      }
    }
}
複製程式碼

compose內部的中介軟體的呼叫邏輯見上文註釋不在複述,下面說一下為什麼koa中介軟體執行是洋蔥模型?
見如下程式碼

app.use(middleware = async (ctx, next) => {
  // 程式碼1
  await next();
  // 程式碼2
});
複製程式碼

當middleware中介軟體執行時,會先執行程式碼1,再執行await next(),await會等到next返回的Promise狀態變為resolve之後再執行程式碼2
執行順序為:程式碼1 => 其他中介軟體(middleware2 => middleware3 => ... ) => 程式碼2

洋蔥是由很多層組成的,你可以把每個中介軟體看作洋蔥裡的一層,根據app.use的呼叫順序中介軟體由外層到裡層組成了整個洋蔥,整個中介軟體執行過程相當於由外到內再到外地穿透整個洋蔥

講完compose,接下來來看下this.callback裡面的handleRequest方法,呼叫方式http.createServer(handleRequest)

    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res);   // 對原生的req,res進行擴充套件封裝成ctx物件
      return this.handleRequest(ctx, fn);         // 處理請求(執行中介軟體並設定res物件)
    };
複製程式碼

handleRequest內部呼叫了createContext和handleRequest, createContext方法會通過在context物件擴充套件一些常用物件生成ctx對像。koa通過攔截get和set操作來實現代理(類似Object.defineProperty)
例如:ctx攔截了body的get和set,實現了對ctx.response的代理。對ctx.body的取值和賦值,實際操作的是ctx.response.body。好處就是將response的邏輯分離到了response.js中

handleRequest方法會呼叫this.handleRequest,程式碼如下

  handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    res.statusCode = 404;                         // 沒有呼叫response.writeHead時的預設響應狀態碼
    const onerror = err => ctx.onerror(err);      // 中介軟體的錯誤處理
    const handleResponse = () => respond(ctx);    // 處理請求,根據請求返回正確的狀態碼和內容
    onFinished(res, onerror);                     // Execute a callback when a HTTP request closes, finishes, or errors.
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);   // fnMiddleware為compose(this.middleware)返回的Promise
  }
複製程式碼

fnMiddleware(ctx).then(handleResponse).catch(onerror)可以理解為3個步驟:

  1. fnMiddleware(ctx):開始執行第一個中介軟體(可通過next呼叫下一個中介軟體)
  2. then(handleResponse):一般中介軟體中我們會根據請求來設定ctx.body等欄位,中介軟體呼叫結束之後,koa根據會根據ctx物件來對設定response(響應的相關內容)。例如handleResponse中會通過response.end(body)或者body.pipe(res)來設定響應內容體
  3. catch(error):捕獲中介軟體執行時可能發生的異常

koa作為web框架,提供了一種可控制非同步流程的中介軟體呼叫方式,並根據中介軟體處理後的結果來設定響應的相關內容

結語

本文大致講了一下koa的執行流程,更多細節見原始碼註釋

另附koa-router原始碼解析

相關文章