Koa源碼筆記

Koa是基於Node.js的web框架,利用它可以很快搭建起一個http服務。本來是要利用它驗證下http協議中某些細節問題,但是發現源碼不多,就順帶看了下。

Koa作爲一個第三方模塊,對外僅暴露了 ApplicationHttpError。 我們僅討論ApplicationHttpError實際爲第三方模塊,用於給Koa添加自定義的http錯誤處理方法)。

Application是一個class可看做Koa自身,先看下Koa實例上有哪些方法:
在這裏插入圖片描述

上面的這些方法除了inspecttoJSON這兩個輔助方法外,其餘方法都應用於web服務的運行。

Koa程序流程

requirements

  1. 利用node創建web服務,主要依靠http模塊(https/http2類似)。需要了解下http模塊的中createServerlisten方法的使用;

簡單說下Koa創建web服務做的事情:創建一個http服務,監聽系統分發到該服務上客戶端請求並響應該請求。具體流程如下:

  1. http.createServer創建一個http服務進程,這個方法接收一個函數作爲有http請求時回調;
  2. app.callback 組合middleware並返回了一個函數fn,這個函數fn處理http請求;
  3. 當有http請求到達服務端時,在函數fn中調用createContext方法創建上下文ctx
  4. 將創建的上下文ctx交給middleware被compose後產生的函數fn處理, middleware裏存放了我們所有的業務邏輯;

Koa提供的只有這個麼一個流程架子,在web服務中經常用到的router,bodyparser,cookie等都交給了自定義的middleware處理。

而這個處理流程的抽象出來的模型就是Koa中總提到的洋蔥模型

Koa洋蔥模型解釋

洋蔥模型的實現靠的是koa-compose模塊。 這是一個compose方法,koa-compose組合函數實現了處理流程的嵌套,我們先看一個不帶嵌套的compose方法,對compose形成一個大概的概念。

// 接收多個函數作爲參數,並返回一個新的函數
function compose(...fn){
    return  function(...args){
        var ret;
        // 依次執行每個函數
        for(var i=0; i < fn.length; i++){
            ret = fn[i].apply(this, args)
        }
        return ret;
    }
}

那麼帶嵌套的compose是什麼意思? 在Koa中,use方法用於註冊一個middleware,其參數是一個函數fn。use方法把這些函數都存放到app.middleware中,最終利用koa-compose講這些函數組合成一個新的函數。函數fn接收兩個參數ctxnext,看官方圖的示例:

在這裏插入圖片描述

可以看得出,next實際上是調用了下一個中間件。當下一個中間件執行完,在回到當前中間件,繼續執行next()後面的語句。這樣一來,就形成了中間件的嵌套。

function compose(...fn){
    if( fn.length === 0 ) return function(){}
    function dispatch(i){
        var next = i+1 < fn.length ? dispatch(i+1) : function(){}
        return function(ctx){
            fn[i]( ctx, function(){ next(ctx) })
        }
    }
    return dispatch( 0 )
}

根據源碼簡單解釋下,嵌套的compose就是把要進行組合的若干的函數,可在未執行完時,先去執行後面的函數,等後面的函數執行完畢,再繼續執行後續未執行的部分。

而Koa使用的koa-compose,不僅支持了嵌套,還支持異步。把每一個函數的都變成異步函數然後通過then方法鏈接起來。

function compose(...fn){
    if( fn.length === 0 ) return Promise.resolve();
    function dispatch(i){
        var next = i+1 < fn.length ? dispatch(i+1) : function(){}
        return new Promise(function(resolve, reject){
            function(ctx){
                fn[i]( ctx, function(){ next(ctx)  })
            }
        })
    }
    return dispatch( 0 )
}

至此應該已經理解洋蔥模型了,它本質上是解決問題的一種固定模式。如果不理解,以Koa中間件的寫法爲例,可簡單粗暴的理解爲,洋蔥左半部分的是next()方法之前的處理流程,右半部分爲next()之後的處理流程。

也可以把http請求當成一個流stream,每個中間件都有兩個門,一個進來的門,一個出去的門。如果一箇中間件中存在next(),則意味着在兩個門之間又調用了其它中間件。
不過next()語句的位置也很值得玩味。在中間件中:

  1. 如果next()的位置都出在函數最後,則這些中間件可以理解爲順序執行;
  2. 如果next()都處在函數最前面,則這些中間件按註冊順序倒序執行;
  3. 如果next()都在函數中間,則這些中間件構成一個典型的洋蔥模型;

在這裏插入圖片描述

Koa洋蔥模型解決的問題

首先考慮下,如果不使用Koa目前的這種方式,有什麼其它方法可以更好地擴展機制用來處理客戶端發來的請求。

我們問題的場景:

我們基於node的http.createServer(callback)創建了一個http服務,用於監聽客戶端發來的請求request,其中callback用於處理request並作出response。也就是說所有的處理邏輯都在這個callback中。OK,我們的問題就是把路由、登錄狀態、日誌、數據查詢等邏輯組合到這個callback中。那麼你會怎麼處理呢??

我總結下下來的koa使用洋蔥模型的好處:

  1. 利用Promise.then處理異步,實現多個異步中間件的有序執行(實際上,是將所有的中間件改爲異步並通過then方法一個個串聯起來);
  2. 中間件機制爲程序處理http請求,提供了更加便捷的擴展方式;
  3. 支持異步程序的同步寫法,更便於不同邏輯間的組合;

Promise與Generator及async/await

  1. Promise ----> ES6/ES2015
  2. Generator ----> ES7/ES2016
  3. async/await ----> ES8/ES2017

Generator 轉 async/await

有一個generator函數 g如下:

function* g(start){
    var x = yield start+1;
    var y = yield x+2;
    return y;
}

Generator函數返回的結果和用法不再贅述。我們的目的是要把這個轉成async/await, 也就意味不需要我們手動調用next方法就可以獲得結果。所以需要有自動調用next方法的機制。

function co(fn, ...args) {
    var g = fn.apply(this, args);

    return new Promise(function (resolve, reject) {
        function step(ret) {
            var result = g.next(ret);
            if (result.done) {
                return resolve(result.value)
            } else if( result.value.next){
                result.value.then( step )
            } else {
                step( result.value)
            }
        }
        step()
    });
}

delegate Node原生 request/response

Object.defineProperty/__defineGetter__/__defineSetter__的關係

node request中的socket

request.socket指向底層套接字, 網絡中進程進行通信的一種機制。相對應的就有本地進程進行通信的機制(IPC,共享內存)。

HTTP協議相關

headersSent

存在於node.js response 上,布爾值(只讀)。 如果已發送響應頭,則爲 true,否則爲 false。這個涉及到http協議中數據發送流程的細節。

vary是什麼

The “Vary” header field in a response describes what parts of a request message, aside from the method, Host header field, and request target, might influence the origin server’s process for selecting and representing this response.

可以說vary爲客戶端提供了明確告知服務端不需要使用緩存的機制(通過指定哪些header變化會導致服務端重新提供響應內容)。
更多信息參考http spec(7.1.4. Vary)Best Practices for Using the Vary Header

http響應body爲空的三個c890-/kiode碼

  • 204: 只需知道響應是否成功,不跳轉
  • 205: 清空當前頁面html中所有的表單元素;
  • 304: 緩存可用,無需返回新的內容;

proxy 與 X-Forwarded-Host

X-Forwarded-Host:請求的源頭服務器,當有代理服務器介入時,代理可能會導致原有的客戶端信息丟失,可以使用此字段存儲原始信息。

http/2

Koa支持 http/2的方式

To implement this and just use app.callback.

const fs = require('fs');
const http2 = require('http2');
const Koa = require('koa');

const app = new Koa();

app.use(ctx => {
  ctx.body = 'Hello Koa'; // or any other stream
});

const options = {
  key: fs.readFileSync('xxx.key'),
  cert: fs.readFileSync('xxx.crt'),
};
const server = http2.createSecureServer(options, app.callback());

server.listen(443);

Status message

http/2不允許自定義status message,意味着每個規範中的code值都有固定的description。

僞頭部字段

HTTP/1.x 使用消息開始行(RFC7230 Section 3.1)傳遞目標URL請求方法響應狀態碼等信息。HTTP/2使用特殊的以":"開始的僞頭部字段來達到這個目的。不屬於常規HTTP頭部字段,不允許終端自己產生,只允許規範中所定義的5個:

  • :method
  • :path
  • :scheme
  • :authority
  • :status

在這裏插入圖片描述

try catch在什麼情況下會比較明顯的影響到性能

V8很早已經對try catch做了性能優化(TurboFan,隨 Chrome 59 發佈),影響基本可以忽略不計。
V8: Behind the Scenes (November Edition feat. Ignition+TurboFan and ES2015)

在這裏插入圖片描述

根據ECMA文檔可知,try catch性能消耗主要原因在於對當前詞法作用域的額外拷貝,這個過程是跟業務無關的,必然會或多或少犧牲掉部分性能。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章