在使用 express.js 當作 Nodejs server 框架時,時常會需要寫一些 Middleware 處理 Token 驗證、用戶權限檢查等等,也會套用很多第三方的模組去建構程式
但突然某天在思考如何自己寫一個紀錄response time
的 Middleware,發現自己沒辦法用一個 Middleware 註冊就完成這件事,因為 express.js 不像是 Koa 的 middleware 是用 promise based 實作,所以當某個環節是非同步,執行的順序就會錯亂
後來查看了 morgan
被大量使用的 express log middleware,才發現其中設計的小巧思,以下是整理的內容
Expressjs Middleware 設計
先來看最基本的 Middleware 設計
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
const express = require ("express" );
const app = express ();
app .use (function (req , res , next ){
console .log ("middleware 1 start" );
next ();
console .log ("middleware 1 end" );
});
app .use (function (req , res , next ){
console .log ("middleware 2 start" );
next ();
console .log ("middleware 2 end" );
});
app .get ("/" , async function (req , res ){
console .log ("request started" )
await delay ();
console .log ("request finished" )
res .send ();
});
async function delay (){
return new Promise((res )=>{
setTimeout (()=>{
res ()
}, 1000 )
})
}
app .listen (3000 );
目前的 log 會變成
middleware 1 start
middleware 2 start
request started
middleware 2 end
middleware 1 end
request finished
如果我們希望在 Request 進來先紀錄開始時間,接著在 Response 結束時紀錄結束時間,就必須仰賴其他的實作方式
爬過 morgan
程式碼後,發現是透過這兩個模組去實作功能的
on-headers :註冊事件,當 header 被寫入時會觸發
on-finished :註冊事件,當 request/response 結束時觸發
這兩個模組可以針對 Nodejs 原生的 http server 搭配使用,express.js 也是繼承原生的 http server
在 morgan
module 中,在 middleware 進入一開始標記 request 開始時間,在 on-headers 時紀錄 request 結束時間,在 on-finished 將訊息印出,pseudo code 大致如下
1
2
3
4
5
6
7
8
9
10
11
12
app .use (function (req , res , next ) {
res ._startTime = new Date().getTime ();
onHeader (res , function () {
res ._endTime = new Date().getTime ();
})
onFinished (res , function () {
console .log (`req process time: ${ res ._endTime - res ._startTime } ms` )
})
next ()
})
log 結果是
request started
request finished
req process time: 1010 ms
將原本的 response 中的 writeHead 複寫,只是多包一層觸發事件的機制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function createWriteHead (prevWriteHead , listener ) {
var fired = false
// return function with core name and argument list
return function writeHead (statusCode ) {
// set headers from arguments
var args = setWriteHeadHeaders .apply (this , arguments )
// fire listener
if (! fired ) {
fired = true
listener .call (this )
....
}
return prevWriteHead .apply (this , args )
}
}
function onHeaders (res , listener ) {
if (! res ) {
throw new TypeError ('argument res is required' )
}
if (typeof listener !== 'function' ) {
throw new TypeError ('argument listener must be a function' )
}
res .writeHead = createWriteHead (res .writeHead , listener )
}
on-finished 實作
這邊的實作就比較有趣,如何正確的判讀 http request/response 結束了呢?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function isFinished (msg ) {
var socket = msg .socket
if (typeof msg .finished === 'boolean' ) {
// OutgoingMessage
return Boolean(msg .finished || (socket && ! socket .writable ))
}
if (typeof msg .complete === 'boolean' ) {
// IncomingMessage
return Boolean(msg .upgrade || ! socket || ! socket .readable || (msg .complete && ! msg .readable ))
}
// don't know
return undefined
}
如果是套用在 response,需注意根據官方文件 response.finished 是指說 res.end()
被呼叫後設定為 true,不代表 response 中的資料完全傳輸到網路上
這些討論可以看 PR response is only finished if socket is detached #31 ,提交者修改成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (stream && typeof stream .closed === 'boolean' ) {
// Http2ServerRequest
// Http2ServerResponse
return stream .closed
}
if (typeof msg .finished === 'boolean' ) {
// OutgoingMessage
return (
msg .finished &&
msg .outputSize === 0 &&
(! socket || socket .writableLength === 0 )
) || (socket && ! socket .writable )
}
增加 http2 的檢查,以及確保 outputSize === 0
所有 queued 住的資料都確實送出 socket
知道如何判斷 response 是否結束,最後看事件的註冊
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function attachFinishedListener (msg , callback ) {
var eeMsg
var eeSocket
var finished = false
function onFinish (error ) {
eeMsg .cancel ()
eeSocket .cancel ()
finished = true
callback (error )
}
// finished on first message event
eeMsg = eeSocket = first ([[msg , 'end' , 'finish' ]], onFinish )
function onSocket (socket ) {
// remove listener
msg .removeListener ('socket' , onSocket )
....
eeSocket = first ([[socket , 'error' , 'close' ]], onFinish )
}
if (msg .socket ) {
// socket already assigned
onSocket (msg .socket )
return
}
// wait for socket to be assigned
msg .on ('socket' , onSocket )
....
}
在 response 與 response.socket 分別註冊事件,當結束或錯誤事件觸發後,檢查 response 是否真的結束,最後觸發用戶註冊的事件
Please enable JavaScript to load the comments.