前幾天,我們已經把 Request 慢慢整理成比較像 Framework 會使用的形式。
例如原本 Browser 傳來:
GET /hello HTTP/1.1
我們可以從裡面取得:
Method = GET
Path = /hello
Version = HTTP/1.1
接著也開始理解 Headers。
但如果今天網址不是:
/hello
而是:
/hello?name=Andy
那問題就來了。
這整段到底都是 Path 嗎?
如果不是,那 Framework 要怎麼知道:
/hello
是網址真正的 Path,
而:
name=Andy
又是另外一種資料?
所以今天想理解的問題就是:
網址後面的 ?name=Andy,到底是什麼?
Framework 又是怎麼把它解析出來的?
💭 我原本以為……
以前看到網址:
/products?id=10
或:
/search?q=book
大概知道問號後面是在帶一些資料。
但以前不太會特別去想:
這些資料到底是怎麼進到程式裡的?
尤其在 Framework 中,我們常常可以很方便地取得 Query 的值。
所以很容易有一種錯覺:
好像 Browser 本來就是把:
Path = /hello
name = Andy
這樣整理好之後再傳過來。
但其實不是。
Browser 真正送來的 Request Line 可能會是:
GET /hello?name=Andy HTTP/1.1
所以 Server 一開始看到的,其實還是一整段:
/hello?name=Andy
Framework 必須自己把它拆開。
網址中的 ? 到底代表什麼?
例如:
/hello?name=Andy
可以先拆成:
/hello
↓
Path
name=Andy
↓
Query String
問號 ? 可以看成一個分界。
它前面的部分代表 Request 要存取的 Path。
它後面的部分則是 Query String。
所以:
/hello?name=Andy
並不是一整個單純的 Path。
而是:
Path = /hello
Query String = name=Andy
這件事對 Routing 很重要。
為什麼對 Routing 很重要?
假設我們的 Route Table 裡面有:
/hello
代表這個 Route 可以找到 Hello Handler。
但 Browser 今天送來:
/hello?name=Andy
如果 Framework 直接拿整段:
/hello?name=Andy
去 Route Table 裡找,
可能就會變成:
Route Table 裡只有 /hello
↓
收到 /hello?name=Andy
↓
比對失敗
↓
找不到 Route
但其實使用者真正想去的 Route 還是:
/hello
name=Andy 只是額外附加的資料。
所以在 Routing 之前,Framework 應該先把 Request Target 拆開。
GET /hello?name=Andy HTTP/1.1
↓
Request Target
↓
/hello?name=Andy
↓
拆開
↓
Path = /hello
Query String = name=Andy
這樣 Router 才能拿:
/hello
去進行 Route Matching。
Query String 裡面又是什麼?
如果只有:
name=Andy
其實還算簡單。
可以理解成:
Key = name
Value = Andy
但 Query String 不一定只有一個值。
例如:
/products?category=book&page=2
問號後面是:
category=book&page=2
這裡其實有兩組資料:
category = book
page = 2
其中:
&
可以理解成不同 Query Parameter 之間的分隔符號。
而:
=
則是 Key 和 Value 之間的分隔。
所以:
category=book&page=2
可以一步一步拆成:
category=book
page=2
再變成:
category → book
page → 2
最後 Framework 就可以整理成比較容易使用的形式。
例如:
request.Query["category"] = "book"
request.Query["page"] = "2"
Query String 和 Query Parameter 一樣嗎?
這兩個很容易混在一起。
Query String 可以理解成問號後面的整段原始文字。
例如:
/products?category=book&page=2
其中 Query String 是:
category=book&page=2
而裡面的:
category=book
和:
page=2
則可以視為個別的 Query Parameters。
所以流程可以理解成:
URL
↓
/products?category=book&page=2
↓
Path = /products
↓
Query String = category=book&page=2
↓
解析
↓
category = book
page = 2
HttpRequest 可以怎麼變得更完整?
前面我們規劃的 HttpRequest 第一版,可能只有:
Method
Path
Version
接著 Day17 加入 Headers。
現在 Day18 就可以再多出:
QueryString
甚至進一步有:
Query
最後概念可能變成:
HttpRequest
├── Method
├── Path
├── Version
├── Headers
├── QueryString
└── Query
例如 Browser 傳:
GET /hello?name=Andy HTTP/1.1
Framework 解析後:
Method = GET
Path = /hello
Version = HTTP/1.1
QueryString = name=Andy
Query["name"] = Andy
這就比直接保留:
/hello?name=Andy
好使用很多。
為什麼不要每次自己 Split?
理論上,Handler 自己做也可以。
例如收到:
/hello?name=Andy
之後,每一個 Handler 都自己找 ?,再自己找 =。
但這樣會有一個和前幾天完全一樣的問題:
Parsing 的細節會散落到整個 Framework。
例如:
Router
↓
自己拆 Query
Handler A
↓
自己拆 Query
Handler B
↓
也自己拆 Query
最後每個地方都必須知道 URL 的底層格式。
比較合理的方向應該是:
Raw Request
↓
HttpRequest Parsing
↓
Path
QueryString
Query
↓
Routing / Handler 使用
這樣只有 HttpRequest 需要知道 Query 的解析方式。
其他地方只需要:
request.Path
或:
request.Query["name"]
就可以了。
🧠 今天的認知更新
以前的我可能會把:
/hello?name=Andy
整體都當成網址。
現在我開始可以拆得更細:
/hello?name=Andy
↓
Request Target
↓
Path + Query String
↓
Path = /hello
Query String = name=Andy
↓
Query Parameter
↓
name = Andy
也就是說:
網址列裡看到的一整段內容,到了 Framework 裡面其實會被拆成很多不同概念。
這些概念最後又會被整理成 Request 物件裡不同的 Property。
📚 官方怎麼說?
HTTP Request 的 Request Target 可以包含 Path 和 Query。
在 Web Framework 中,通常也會把 Path 與 Query 分開提供。
例如 ASP.NET Core 的 Request 物件中,就有 Path 和 QueryString 相關資訊,而 Query 也可以用來取得已解析的 Query Parameter。
所以今天自己思考的方向,其實和真正 Framework 的設計很接近。
Framework 並不是直接把整段:
/hello?name=Andy
當成單純的 Route。
而是先把不同資訊拆開,再分別交給 Routing 或其他功能使用。
🔍 今日最大的發現
今天最大的發現是:
Route 和 Query 其實是兩件不同的事情。
例如:
/hello?name=Andy
真正拿去 Routing 的應該是:
/hello
而:
name=Andy
則是這次 Request 附帶的額外資料。
所以整個流程應該更像:
Browser
↓
GET /hello?name=Andy HTTP/1.1
↓
Parsing
↓
Path = /hello
Query = name: Andy
↓
Routing 使用 Path
↓
Handler 使用 Query
這也讓我更理解,Framework 不只是「幫我收到 Request」。
它還要把 Request 裡不同用途的資料分類整理。
Framework 收到:
/hello?name=Andy
之後,不應該把它當成一整個 Path。
而是先拆成:
Path = /hello
Query String = name=Andy
接著再把 Query String 解析成:
name = Andy
最後 Routing 使用 Path,而 Handler 可以使用 Query。
🛠 與 Mini Web Framework 的關聯
目前 Mini Web Framework 的 Request 已經越來越不像單純的一串字。
原本:
Raw HTTP Request
後來慢慢變成:
HttpRequest
├── Method
├── Path
├── Version
├── Headers
└── Query
整個 Request 流程也開始變成:
Browser
↓
Raw HTTP
↓
HttpRequest.Parse()
↓
Method
Path
Version
Headers
Query
↓
Routing
↓
Handler
這代表我們自己做的 Framework 開始真正負責:
把 HTTP 底層格式轉換成程式比較好使用的資料。
結尾
今天雖然只是研究網址後面的一個問號,但其實讓我更清楚理解 Request Parsing 的重要性。
以前看到:
/hello?name=Andy
可能只會覺得這就是一個網址。
但現在可以明確拆成:
/hello
↓
Path
name=Andy
↓
Query String
name → Andy
↓
Query Parameter
而且這個拆分會直接影響 Routing。
因為真正決定要執行哪一個 Handler 的是 Path,而 Query 則是 Request 額外攜帶的資料。
也就是:
Path 決定「要去哪裡」
Query 描述「這次還帶了什麼資料」
當 Framework 把這些底層資訊整理好後,開發者才可以方便地使用:
request.Path
和:
request.Query
而不需要每一次都重新解析 URL。
❓下一個 Why
現在我們已經開始把 Request 拆得越來越完整。
Method、Path、Version、Headers、Query 都慢慢出現了。
但還有一個很重要的東西一直沒真正處理:
Request Body。
像 POST 表單或 JSON 資料,通常就不會全部塞在網址裡。
那 Browser 傳來的 Body 到底放在哪裡?
Server 又要怎麼知道 Body 有多長、要怎麼把它讀完整?