Koa是基於Node.js的web框架,利用它可以很快搭建起一個http服務。本來是要利用它驗證下http協議中某些細節問題,但是發現源碼不多,就順帶看了下。
Koa作爲一個第三方模塊,對外僅暴露了 Application
和HttpError
。 我們僅討論Application
(HttpError
實際爲第三方模塊,用於給Koa添加自定義的http錯誤處理方法)。
Application
是一個class
可看做Koa自身,先看下Koa實例上有哪些方法:
上面的這些方法除了inspect
和toJSON
這兩個輔助方法外,其餘方法都應用於web服務的運行。
Koa程序流程
requirements
- 利用node創建web服務,主要依靠
http
模塊(https/http2類似)。需要了解下http
模塊的中createServer
及listen
方法的使用;
簡單說下Koa創建web服務做的事情:創建一個http服務,監聽系統分發到該服務上客戶端請求並響應該請求。具體流程如下:
http.createServer
創建一個http服務進程,這個方法接收一個函數作爲有http請求時回調;app.callback
組合middleware並返回了一個函數fn
,這個函數fn
處理http請求;- 當有http請求到達服務端時,在函數
fn
中調用createContext
方法創建上下文ctx
; - 將創建的上下文
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
接收兩個參數ctx
和next
,看官方圖的示例:
可以看得出,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()
語句的位置也很值得玩味。在中間件中:
- 如果
next()
的位置都出在函數最後,則這些中間件可以理解爲順序執行; - 如果
next()
都處在函數最前面,則這些中間件按註冊順序倒序執行; - 如果
next()
都在函數中間,則這些中間件構成一個典型的洋蔥模型;
Koa洋蔥模型解決的問題
首先考慮下,如果不使用Koa目前的這種方式,有什麼其它方法可以更好地擴展機制用來處理客戶端發來的請求。
我們問題的場景:
我們基於node的
http.createServer(callback)
創建了一個http服務,用於監聽客戶端發來的請求request,其中callback用於處理request並作出response。也就是說所有的處理邏輯都在這個callback中。OK,我們的問題就是把路由、登錄狀態、日誌、數據查詢等邏輯組合到這個callback中。那麼你會怎麼處理呢??
我總結下下來的koa使用洋蔥模型的好處:
- 利用
Promise.then
處理異步,實現多個異步中間件的有序執行(實際上,是將所有的中間件改爲異步並通過then
方法一個個串聯起來); - 中間件機制爲程序處理http請求,提供了更加便捷的擴展方式;
- 支持異步程序的同步寫法,更便於不同邏輯間的組合;
Promise與Generator及async/await
- Promise ----> ES6/ES2015
- Generator ----> ES7/ES2016
- 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
性能消耗主要原因在於對當前詞法作用域的額外拷貝,這個過程是跟業務無關的,必然會或多或少犧牲掉部分性能。