前幾天,我們一直在把整個 Request 到 Response 的流程補完整。
目前已經可以想像一個 Request 進來後:
Browser
↓
HttpRequest
↓
Routing
↓
Parameter Binding
↓
Handler
↓
Result Conversion
↓
HttpResponse
↓
Browser
看起來整條流程已經很完整了。
但這時又出現一個很實際的問題。
假設每一次 Request 進來,我都想先記錄:
誰來了?
打了哪個 Path?
用了什麼 Method?
花了多少時間?
或者某些功能需要先確認:
使用者有沒有登入?
Token 合不合法?
發生 Exception 時要不要統一處理?
難道每個 Handler 都要自己重複寫一次嗎?
例如:
Hello Handler
先 Logging
再驗證
再執行真正邏輯
Products Handler
先 Logging
再驗證
再執行真正邏輯
Users Handler
又再寫一次 Logging
又再寫一次驗證
這樣 Framework 很快又會出現大量重複程式碼。
所以今天想理解的問題就是:
如果很多 Request 都要經過同樣的處理,Framework 到底怎麼把這些共用邏輯放在一起?
這就是 Middleware 開始出現的地方。
💭 我原本以為……
以前使用 ASP.NET Core 時,我常常看到:
app.Use(...)
或者一些:
UseAuthentication
UseAuthorization
UseExceptionHandler
以前可能知道:
這些東西要加在 Program.cs 裡。
但沒有真的理解:
為什麼要叫 Middleware?
也不知道它們到底插在 Request 的哪個位置。
只會覺得:
Framework 好像有很多「功能開關」。
但現在自己把 Request Pipeline 一步一步拆完後,突然比較能理解。
Middleware 並不是單純一個設定。
它其實是在:
Request 到達 Handler 之前或之後,
插入一段共用的處理流程。
Middleware 到底是什麼?
目前可以先把 Middleware 理解成:
Request Pipeline 中的一個處理節點。
例如:
Request
↓
Middleware A
↓
Middleware B
↓
Routing
↓
Handler
每一個 Middleware 都可以:
先做一些事情
決定要不要繼續往下走
也可以在下游處理完成後,再做一些事情
所以 Middleware 並不只是:
Request 前的程式碼。
它甚至可以包住後面的整個流程。
最簡單的 Logging Middleware 怎麼想?
假設我想記錄每個 Request。
如果沒有 Middleware,可能每個 Handler 都自己寫:
收到 Request
開始計時
執行 Handler
結束計時
印出時間
這會很重複。
如果有 Middleware,就可以變成:
Request
↓
Logging Middleware
↓
記錄開始時間
↓
交給下一個處理流程
↓
Handler 執行
↓
回到 Logging Middleware
↓
記錄結束時間
↓
Response
所以 Logging 不需要寫進每個 Handler。
只要所有 Request 都會經過同一個 Middleware,就能統一處理。
為什麼 Middleware 可以「回來」?
這是 Middleware 一開始比較容易搞混的地方。
它不是只有:
由上往下跑完。
更像是:
先往下呼叫下一層
等下一層完成
再回到自己
例如:
Middleware A 開始
↓
Middleware B 開始
↓
Handler
↑
Middleware B 結束
↑
Middleware A 結束
所以整個 Pipeline 很像一層一層包起來。
可以想成:
A(
B(
Handler
)
)
Request 進去時:
A → B → Handler
Response 回來時:
Handler → B → A
這也是為什麼 Middleware 可以同時處理:
Request 前
和
Response 後
的工作。
Logging 就是一個很典型的例子。
Exception Handling 為什麼很適合 Middleware?
假設 Handler 裡發生錯誤。
如果沒有統一處理,每個 Handler 都可能要自己:
try
catch
再建立:
500 Internal Server Error
但如果把 Exception Handling 放在 Middleware:
Exception Middleware
↓
try
↓
next()
↓
後面的所有處理
如果裡面任何地方丟 Exception:
Handler
↓
throw
就會一路回到外層的 Exception Middleware。
它就可以統一:
catch Exception
↓
記錄錯誤
↓
建立 500 Response
所以 Middleware 很適合處理:
Cross-Cutting Concerns
也就是「很多功能都會需要,但又不屬於任何一個特定 Handler 的共用關注點」。
Authentication 也是同樣概念
假設某些 Request 需要登入。
如果每一個 Handler 都自己:
讀 Authorization Header
解析 Token
確認是否合法
那會非常麻煩。
比較好的方式是:
Request
↓
Authentication Middleware
↓
讀取 Header
↓
驗證身分
↓
建立 User Context
↓
next()
↓
Routing / Handler
後面的 Handler 就不需要再重新做一次完整驗證。
它只需要知道:
這個 Request 的使用者是誰。
所以 Middleware 可以把底層驗證邏輯放在 Handler 之前統一完成。
Middleware 可以不呼叫下一層嗎?
可以。
這點非常重要。
假設 Authentication Middleware 發現:
Token 無效。
那它根本沒有必要繼續:
Routing
↓
Handler
可以直接回:
401 Unauthorized
也就是:
Request
↓
Authentication Middleware
↓
驗證失敗
↓
401 Response
↓
結束
這種情況通常稱為:
Short-Circuit
也就是 Middleware 直接終止 Pipeline。
所以 Middleware 不只是:
「一定做完再交給下一層」。
它其實有權決定:
這個 Request 還要不要繼續往下走。
這讓 Pipeline 變得非常有彈性。
如果 Authentication 通過呢?
就可以:
Authentication Middleware
↓
成功
↓
next()
↓
後面的 Pipeline
所以 Middleware 很像:
每一個關卡都可以決定:
通過
或
直接結束。
Pipeline 又是什麼?
前面一直提到:
Request Pipeline
現在可以更具體理解。
Pipeline 就是一連串按照順序執行的處理步驟。
例如:
Request
↓
Exception Middleware
↓
Logging Middleware
↓
Authentication Middleware
↓
Routing
↓
Handler
↓
Result Conversion
↓
Response
每一層都有不同責任。
而 Framework 的工作之一,就是:
把這些處理步驟按照順序串起來。
所以 ASP.NET Core 裡常看到:
UseA
UseB
UseC
順序會重要。
因為它其實就是在建立:
A
↓
B
↓
C
↓
Endpoint
順序真的會影響結果嗎?
會。
假設:
Authentication Middleware
放在 Routing 前或後,
可能會影響它能不能取得某些 Endpoint 資訊。
又或者 Exception Middleware 如果放得太裡面,
就可能抓不到外層發生的 Exception。
所以 Middleware Pipeline 並不是:
「有加就好」。
而是:
執行順序本身就是設計的一部分。
這也是為什麼 Framework 文件常常會特別告訴開發者:
某些 Middleware 要放在哪一個 Middleware 前面或後面。
next 到底是什麼?
在 Middleware 的概念裡,常常會看到一個:
next
目前可以先把它理解成:
下一個 Middleware,或 Pipeline 後面的剩餘部分。
例如:
Middleware A
裡面:
做 A 的前置工作
↓
next()
↓
做 A 的後置工作
當呼叫:
next()
其實就是:
把控制權交給 Pipeline 的下一層。
所以:
A → next
可能進到:
B
而 B 再呼叫:
next
才會進到:
C
最後可能到:
Handler。
這就是 Middleware Chain。
它和 Delegate 很有關係
做到這裡,其實又會看到之前學過的 Delegate。
因為:
next
本質上就是:
一段等等可以被呼叫的程式。
這和 Day13 用:
Action
Func
的概念很接近。
例如概念上:
Middleware(next)
會回傳一段新的處理程式。
它裡面先做自己的事情,
再決定要不要呼叫:
next()
所以 Middleware Pipeline 可以透過 Delegate 一層一層組合起來。
這也讓之前學 Delegate 的內容再次接回 Framework 設計。
Framework 為什麼不能全部寫在 Program.cs?
理論上當然可以。
例如:
收到 Request
先 Logging
再 Authentication
再 Routing
再 Handler
再 Error Handling
全部寫在 while(true) 裡。
功能也能做。
但很快會變成:
Program.cs
幾百行
甚至上千行。
而且各種責任混在一起。
Middleware 的價值就是把:
Logging
Authentication
Exception Handling
CORS
Timing
其他共用功能
拆成獨立元件。
然後 Framework 再負責把它們組成 Pipeline。
所以:
Program.cs
不需要知道每個 Middleware 裡面所有細節。
它只需要決定:
Pipeline 的順序。
官方怎麼說?
ASP.NET Core 會使用一系列 Request Delegates 組成 Request Pipeline。
每個 Middleware 都可以:
在下一個 Middleware 執行之前做處理
呼叫下一個 Middleware
在下一個 Middleware 完成後再做處理
也可以選擇不呼叫下一層,直接終止 Request Pipeline。
這就對應到:
Request
↓
Middleware
↓
next
↓
Middleware
↓
Endpoint
↓
Response
因此 Middleware 不只是外掛功能。
它本身就是 Framework Request Processing Architecture 的核心部分之一。
以前的我:
Request
↓
Controller
↓
Response
現在的我:
Request
↓
Middleware Pipeline
↓
Routing
↓
Parameter Binding
↓
Handler
↓
Result Processing
↓
Middleware Pipeline
↓
Response
也就是說:
Handler 並不是 Request Pipeline 的全部。
它只是中間真正處理商業邏輯的一部分。
在它之前和之後,都可以存在很多 Framework 級別的共用處理。
今天最大的發現是:
Middleware 其實是在控制「流程」。
前幾天做的元件比較像:
HttpRequest
→ 描述 Request
HttpResponse
→ 描述 Response
Router
→ 找 Handler
Parameter Binder
→ 準備參數
而 Middleware 處理的是:
這些東西到底要按照什麼順序經過哪些步驟。
也就是:
Framework 不只是需要很多元件。
還需要一個方法把所有元件串起來。
Pipeline 就是在做這件事。
Framework 可以把 Logging、Authentication、Exception Handling 等共用邏輯拆成 Middleware。
每一個 Middleware 接收 Request 後,可以做自己的處理,再把控制權交給下一個 Middleware。
最後形成:
Request
↓
Middleware
↓
Middleware
↓
Handler
↓
Middleware
↓
Middleware
↓
Response
這樣共用邏輯就不用重複寫進每一個 Handler。
目前 Mini Web Framework 已經有很多零件:
HttpRequest
Router
Parameter Binding
Handler
Result Conversion
HttpResponse
現在還需要一件事情:
把它們真正串成一條 Pipeline。
例如未來可能是:
Browser
↓
HttpRequest
↓
Exception Middleware
↓
Logging Middleware
↓
Router
↓
Parameter Binder
↓
Handler
↓
Result Converter
↓
HttpResponse
↓
Browser
如果能做到這裡,
Mini Web Framework 就不再只是:
幾個獨立功能拼在一起。
而是真的開始有一個完整的:
Request Processing Pipeline。
今天暫時不實作
今天依然先理解 Middleware 與 Pipeline 的概念。
因為距離最後五天的成品整合已經很近了。
真正開始做成品時,可以先實作一個最簡單的 Middleware:
Logging Middleware。
例如:
收到 Request
↓
印出 Method 和 Path
↓
呼叫 next
↓
Response 完成
↓
印出處理完成
只要這個最小 Pipeline 成功,就代表:
Framework 已經能把共用處理插進 Request Flow。
之後如果還有時間,再加入:
Exception Handling
或其他簡單 Middleware。
結尾
今天讓我第一次重新看待:
Request → Handler → Response
這條流程。
以前會覺得 Handler 就是整個 Web Application 的中心。
但現在開始發現:
真正的 Framework 在 Handler 外面,其實還有一層很重要的 Pipeline。
很多不屬於特定 Handler 的功能:
Logging
Authentication
Exception Handling
Timing
都可以放在 Middleware 中。
每個 Middleware 不需要知道整個 Framework 的所有細節。
它只需要:
處理現在的 Request
決定要不要呼叫 next
必要時再處理回來的 Response
最後一層一層組合,就形成完整的 Request Pipeline。
這也再次讓我看到 Framework 設計一直重複出現的一個想法:
把不同責任拆開。
再用清楚的方式把它們組回來。
現在我們已經有:
Request
Routing
Parameter Binding
Handler
Response
甚至開始有:
Middleware Pipeline
整個 Framework 的核心零件幾乎都出現了。
但目前很多東西都還只是個別概念。
真正到了最後成品時,Framework 到底應該長什麼樣子?
Program.cs 又應該剩下多少東西?
所以在正式進入最後五天整合前,最後一個很重要的問題是:
一個 Framework 到底該怎麼把這些元件組織起來?