學習Koa

WU_CHONG發表於2018-04-05

原生HTTP伺服器

學習過Nodejs的朋友肯定對下面這段程式碼非常熟悉:

const http = require('http');
let server = http.createServer((req, res) => {
  // ....回撥函式,輸出hello world
  res.end('hello world!')
})
server.listen(3000)
複製程式碼

就這樣簡單幾行程式碼,就搭建了一個簡單的伺服器,伺服器以回撥函式的形式處理HTTP請求。上面這段程式碼還有一種更加清晰的等價形式,程式碼如下:

let server = new http.Server();
server.on("request", function(req, res){
  // ....回撥函式,輸出hello world
  res.end('hello world!')
});
server.listen(3000);
複製程式碼

首先建立了一個HttpServer的例項,對該例項進行request事件監聽,server在3000埠進行監聽。HttpServer繼承與net.Server,它使用http_parser對連線的socket物件進行解析,當解析完成http header之後,會觸發request事件,body資料繼續儲存在流中,直到使用data事件接收資料。

req是http.IncomingMessage例項(同時實現了Readable Stream介面),詳情請參看文件

res是http.ServerResponse例項(同時實現了Writable Stream介面),詳情請參看文件

Koa寫HTTP伺服器

Koa 應用程式是一個包含一組中介軟體函式的物件,它是按照類似堆疊的方式組織和執行的。

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

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

app.listen(3000);
複製程式碼

Koa寫http伺服器的形式與我們直接通過node http模組寫的方式差別很大。第一部分析可知,node的http伺服器建立來自於http.createServer等方法,Koa中是如何從原生方法封裝成koa形式的伺服器呢?搞懂這個原理也就搞懂了Koa框架設計的理念。

Koa原始碼解析

要搞懂這個原理,最好的方法就是直接檢視Koa的原始碼。Koa程式碼寫的非常精簡,大約1700多行,難度並非太大,值得一看。 我們以上述demo為例,進行一個分析,我把koa的執行分為兩個階段,第一個階段:初始化階段,主要的工作為初始化使用到的中介軟體(async/await形式)並在指定埠偵聽,第二個階段:請求處理階段,請求到來,進行請求的處理。

初始化階段

第一個階段主要使用的兩個函式就是app.use和app.listen。這兩個函式存在application.js中。 app.use最主要的功能將中介軟體推入一個叫middleware的list中。

use(fn) {
    ...
    this.middleware.push(fn);
    return this;
  }
複製程式碼

listen的主要作用就是採用我們第一部分的方式建立一個http伺服器並在指定埠進行監聽。request事件的監聽函式為this.callback(),它返回(req, res) => {}型別的函式。

listen(...args) {
    const server = http.createServer(this.callback());
    return server.listen(...args);
}
複製程式碼

分析一下callback函式,程式碼如下:

/**
   * Return a request handler callback
   * for node's native http server.
   *
   * @return {Function}
   * @api public
   */

  callback() {
    const fn = compose(this.middleware); // 將中介軟體函式合成一個函式fn
    // ...
    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res);  // 使用req和res建立一個上下文環境ctx
      return this.handleRequest(ctx, fn); 
    };

    return handleRequest;
  }
複製程式碼

至此第一個階段完成,通過原始碼的分析,我們可以知道它實際執行的內容跟我們第一部分使用node http模組執行的大概一致。這裡有一個疑問,compose函式是怎麼實現的呢?async/await函式返回形式為Promise,怎麼保證它的順序執行呢?一開始我的猜想是將下一個middleware放在上一個middleware執行結果的then方法中,大概思路如下:

compose(middleware) {
        return () => {
          let composePromise = Promise.resolve();   
          middleware.forEach(task => { composePromise = composePromise.then(()=>{return task&&task()}) }) 
          return composePromise; 
        }
    }
複製程式碼

最終達到的效果為:f1().then(f2).then(f3).. Koa在koa-compose中用了另外一種方式:

function compose (middleware) {
  // ...
  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, function next () {
          return dispatch(i + 1)
        }))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}
複製程式碼

它從第一個中介軟體開始,遇到next,就中斷本中介軟體的程式碼執行,跳轉到對應的下一個中介軟體執行期內的程式碼…一直到最後一箇中介軟體,然後逆序回退到倒數第二個中介軟體next下部分的程式碼執行,完成後繼續會退…一直會退到第一個中介軟體next下部分的程式碼執行完成,中介軟體全部執行結束。從而實現我們所說的洋蔥圈模型。

請求處理階段

當一個請求過來時,它會進入到request事件的回撥函式當中,在Koa中被封裝在handleRequest中:

handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    res.statusCode = 404;
    // koa預設的錯誤處理函式,它處理的是錯誤導致的異常結束
    const onerror = err => ctx.onerror(err);
    // respond函式裡面主要是一些收尾工作,例如判斷http code為空如何輸出,http method是head如何輸出,body返回是流或json時如何輸出
    const handleResponse = () => respond(ctx);
    // 第三方函式,用於監聽 http response 的結束事件,執行回撥
    // 如果response有錯誤,會執行ctx.onerror中的邏輯,設定response型別,狀態碼和錯誤資訊等
    onFinished(res, onerror);
    
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }
複製程式碼

請求到來時,首先執行第一個階段封裝的compose函式,然後進入handleResponse中進行一些收尾工作。至此,完成整個請求處理階段。

總結

Koa是一個設計非常精簡的Web框架,原始碼本身不含任何中介軟體,可以使我們根據自身需要去組合一些中介軟體使用。它結合async/await實現了洋蔥模式。

相關文章