前幾篇開始從:
Debug
↓
Component 選擇
↓
Trade-off
慢慢進入比較接近真實專案的工程判斷。
今天換一個第一次看到時,我覺得非常容易滿頭問號的問題:
網址明明寫對了,為什麼卻跑進另一個頁面?
例如我們原本想進:
/users/create
結果 Router 卻把:
create
當成:
id
於是原本想開:
Create Page
卻跑去:
Detail Page
甚至開始打:
GET /users/create
然後 Backend 回:
400
404
第一眼很容易以為:
API 壞了?
但真正的 Root Cause 可能根本還沒走到 API。
而是在:
Route Matching。
假設我們有:
/users/create
這是一個固定網址。
可以稱作:
Static Route
例如:
/users/create
因為:
create
就是固定的文字。
另一條:
/users/:id
這裡:
:id
代表:
這一段可以接不同的值。
例如:
/users/123
/users/456
/users/999
Router 會把:
123
456
999
當成:
id
這種叫:
Dynamic Route
:id 其實不知道什麼叫「ID」這裡是很重要的一點。
人看到:
:id
會自然理解:
這裡應該放資料庫 ID。
例如:
123
但 Router 不一定知道:
id 必須是數字。
對它來說:
/users/123
可能符合。
那:
/users/create
其實在「網址形狀」上也可能符合:
/users/:id
因為:
:id = "create"
完全可以成立。
例如:
/users/:id
其實是在說:
/users/
後面再接一段東西
所以:
/users/123
符合。
/users/abc
也可能符合。
甚至:
/users/create
也有可能符合。
因為對 Dynamic Segment 來說:
create
只是一段字串。
假設我們有:
/users/create
以及:
/users/:id
那網址:
/users/create
對第一條來說:
完全符合
對第二條來說:
:id = "create"
也可能符合。
這時候 Router 就要決定:
到底要選哪一條?
這裡不能直接記:
「Router 永遠由上往下,先寫的先吃。」
因為不同版本、不同 Routing Library,規則可能不同。
有些 Router 會:
依照宣告順序
有些會:
對 Route 做 ranking
也就是比較:
Static Route
Dynamic Route
Wildcard
哪一條更具體。
所以真正該做的不是死背:
一定要放前面
而是:
知道 Static 和 Dynamic Route 可能發生衝突,然後確認目前專案使用的 Router 怎麼 Match。
例如某些 Route 設定可能長得像:
[
{
path: "/users/:id",
component: UserDetail,
},
{
path: "/users/create",
component: UserCreate,
},
]
如果目前的 Routing 機制會優先命中前面的:
/users/:id
那:
/users/create
就可能先被當成:
id = create
這時就會出問題。
例如:
/users/create
/users/edit
/users/import
這些都是:
Static Route
而:
/users/:id
比較廣。
可以把它想成:
Static Route
→ 我就是指定這個名字
Dynamic Route
→ 只要這裡有一段值,我都有可能接
所以在某些 Router 設計裡,
會希望較具體的:
/users/create
不要被較廣泛的:
/users/:id
搶走。
假設使用者開:
/users/create
但 Router Match 到:
/users/:id
那 Detail Page 可能收到:
id = "create";
接著:
Detail Page
↓
取得 id
↓
UserService.getById(id)
最後變成:
GET /users/create
Frontend 可能完全不知道:
我其實根本進錯頁面了。
它只是很正常地:
拿 route param
↓
打 Detail API
所以整條錯誤流程是:
Route Match 錯
↓
進錯 Component
↓
取得錯誤 param
↓
打錯 API
↓
Backend Error
這跟前面幾篇 Debug 的核心很像。
表面看到:
GET /users/create
→ 400
很容易直接查:
Backend Controller
Service
API
但其實應該先問:
為什麼 Frontend 會把
create當成 ID?
往前追:
API Parameter
↑
Route Param
↑
Route Matching
Root Cause 才出現。
例如 Network 看到:
GET /users/create
如果這支 API 本來期待:
/users/{id}
我不會只想:
Backend 怎麼沒有擋 create?
而會回 Frontend 看:
id 從哪裡來?
可能看到:
const { id } = params;
再繼續問:
params 從哪來?
答案:
Route
最後才找到:
/users/:id
我們前面其實一直在練這件事。
例如:
畫面錯
↓
找 State
↓
找 Response
↓
找 API
今天則是:
API Param 錯
↓
找 params
↓
找 Router
↓
找 Route Matching
不要停在錯誤發生的地方。
而是:
往前找到這個錯誤值最早從哪裡進來。
id例如:
/orders/:orderId
/customers/:customerId/edit
/reports/:type
這些都是 Dynamic Segment。
例如:
/reports/daily
可能得到:
type = "daily";
所以:
:
後面的名字其實只是:
我們幫這段網址取的變數名稱。
例如:
function getUser(id) {}
這裡:
id
只是一個變數名稱。
Route:
/users/:id
也是類似概念。
網址:
/users/123
就像:
id = "123";
網址:
/users/create
如果 Match 到同一條,
就變成:
id = "create";
Router 不會因為變數叫:
id
就自動理解:
只能放 Number。
這時可以有不同策略。
例如:
Frontend Route 設計避免衝突
或者:
進入頁面後驗證 param
Backend 當然也應該驗證自己的輸入。
但重點是:
不要因為 Backend 最後會驗證,就讓 Frontend Route 設計保持模糊。
每一層還是應該做好自己的責任。
例如:
/users/create
/users/:id
已經很常見。
也可以把操作放得更明確:
/users/new
/users/:id
/users/:id/edit
真正使用哪一種,
還是看專案規範。
但重點是:
設計 Route 時,要想到哪些 path segment 可能互相撞到。
我會開始看:
這是 Static 還是 Dynamic?
Dynamic Segment 能吃到哪些值?
前面有沒有可能撞到的 Static Route?
這個 Router 的 matching 規則是什麼?
Route Param 最後會流去哪裡?
因為 Route 不只是:
點哪個網址去哪個頁面。
它其實也是資料流的入口。
例如:
/users/123
裡面的:
123
最後可能變成:
params.id
再變成:
UserService.getById(params.id)
所以完整資料流:
URL
↓
Router
↓
Route Params
↓
Component
↓
Service
↓
API
這跟我們之前的:
Input
↓
State
↓
Request
其實很像。
只是資料來源從:
使用者輸入
換成了:
URL。
看到:
/users/:id
不要把:
:id
理解成:
Router 已經知道這是一個真正的資料庫 ID。
更準確的理解是:
這一段網址是一個動態值,而我們把它命名成 id。
所以:
/users/create
在某些 Route Matching 情境下,
也可能被理解成:
id = "create";
Debug 時如果看到:
API 莫名收到奇怪的 ID
除了查 API,
也要往前看:
這個 ID 是不是從 URL / Route Param 來的?
最後再提醒自己:
錯誤出現在 API,不代表 Root Cause 在 API。
它可能早在 Router 決定「這個 URL 應該進哪個頁面」時,就已經發生了。
下一篇可以繼續進另一個很容易「看起來有做,實際上只做一半」的主題:
權限不是把按鈕藏起來就好:Frontend Permission 和 Backend Authorization 到底差在哪?