前幾天我們一直在練:
Request 為什麼多打?
為什麼沒重新打?
Cache 到底在哪一層影響資料?
今天換另一種很常見、而且非常容易讓人誤判的問題:
原本明明是 Array,為什麼進 URL 繞一圈回來,竟然變成 String?
這種 Bug 表面上看起來像:
API 型別錯了
但真正的問題可能發生得更早。
是在:
Form
↓
URL
↓
再還原回 Form / Request
這條路上。
假設使用者選:
["FA", "PA"]
這是一個很正常的 Array。
例如:
const creationMethodIn = ["FA", "PA"];
如果直接送 API,而 Backend 也期待 Array:
{
"creationMethodIn": ["FA", "PA"]
}
完全沒問題。
例如我們想把搜尋條件同步到網址:
?page=1&creationMethodIn=FA,PA
這時 URL 裡面的:
FA,PA
其實只是:
String
不是 JavaScript Array。
也就是:
["FA", "PA"]
進 URL 之後,可能變成:
"FA,PA"
資料型別已經改變。
把 Array 變 String 其實很合理。
因為 URL Query String 本身就是文字格式。
真正的問題常常發生在:
從 URL 還原時,忘記把 String 轉回 Array。
例如網址:
?creationMethodIn=FA
從 URL 讀出來後:
creationMethodIn = "FA";
但原本的多選欄位需要:
["FA"]
如果直接拿:
"FA"
送 API,
Backend 期待:
Array
卻收到:
String
就可能出現:
400 Bad Request
如果網址:
?creationMethodIn=FA,PA
人看起來很像:
兩個值
所以比較容易想到要 split。
但如果只有:
?creationMethodIn=FA
它看起來就非常合理。
很容易直接以為:
"FA"
就是正常資料。
但對多選欄位來說,它真正應該是:
["FA"]
所以這種 Bug 很容易只在:
單選一個選項
時發生。
這裡其實是在做兩件事。
Array
↓
String
這叫做:
Serialize
例如:
["FA", "PA"]
轉成:
"FA,PA"
可以寫:
value.join(",");
String
↓
Array
這叫做:
Deserialize
例如:
"FA,PA"
轉成:
["FA", "PA"]
可以:
value.split(",");
Form
↓
["FA", "PA"]
Serialize
↓
"FA,PA"
URL
↓
?creationMethodIn=FA,PA
Deserialize
↓
["FA", "PA"]
API
↓
Array
只要中間少了:
Deserialize
資料型別就會錯。
例如:
列表頁
↓
選 Filter
↓
URL 記住搜尋條件
↓
進 Detail
↓
按上一頁回列表
這時頁面會重新從 URL 還原 Search Params。
原本:
creationMethodIn = ["FA"];
可能已經被網址存成:
creationMethodIn=FA
回來後如果直接:
{
creationMethodIn: "FA"
}
送 API,
問題就出現了。
所以:
第一次搜尋正常,不代表從 URL 還原後也正常。
如果只測:
進列表
↓
選 Filter
↓
Search
可能完全正常。
但真正的 Bug 流程是:
選 Filter
↓
進 Detail
↓
Back
↓
Search Params 從 URL 還原
↓
API 400
所以測試不能只看:
這個 Component 當下能不能操作。
還要看:
資料離開這個 Component 之後,繞一圈回來還是不是原本的型別。
假設有:
const ARRAY_SEARCH_PARAMS = [
"creationMethodIn",
];
從 URL 讀回來時:
ARRAY_SEARCH_PARAMS.forEach(key => {
if (
next[key] !== undefined &&
!Array.isArray(next[key])
) {
next[key] =
String(next[key]).split(",");
}
});
這段第一次看可能很抽象。
但其實可以拆開。
key 現在是欄位名稱例如:
key = "creationMethodIn";
所以:
next[key]
其實就是:
next["creationMethodIn"]
等同:
next.creationMethodIn
只是因為:
欄位名稱是動態的
所以用:
[]
取值。
next[key] !== undefined
意思:
URL 裡真的有這個條件嗎?
如果沒有:
undefined
就不用處理。
!Array.isArray(next[key])
意思:
它現在還不是 Array 嗎?
如果本來就已經是:
["FA"]
就不要再轉一次。
String(next[key]).split(",");
例如:
next[key] = "FA";
先:
String("FA")
還是:
"FA"
再:
"FA".split(",");
得到:
["FA"]
如果:
"FA,PA"
則:
["FA", "PA"]
假設:
next[key] = ["FA", "PA"];
可以:
next[key] = next[key].join(",");
得到:
"FA,PA"
所以完整概念:
get
String → Array
set
Array → String
如果只有一個欄位:
creationMethodIn
直接寫死也可以。
但如果之後還有:
statusIn
typeIn
companyIn
每一個都各寫:
if (...)
很容易重複。
所以可以把:
哪些 Search Params 本來應該是 Array
集中定義:
const ARRAY_SEARCH_PARAMS = [
"creationMethodIn",
"statusIn",
];
然後統一:
get → normalize
set → serialize
這樣資料邊界會比較清楚。
split(",")這裡也要小心。
如果資料本身可能包含:
,
例如:
"Taipei, Taiwan"
那:
split(",")
就會把原本內容拆壞。
所以:
join(",")
split(",")
適不適合,
要看這個欄位的資料規格。
如果選項值本身保證不包含逗號,
就可以使用。
以前我會把 URL 想成:
就是一個網址。
但做搜尋條件同步後,才發現:
URL 也是資料儲存與傳遞的一層。
例如:
React State
↓
URL String
↓
重新載入
↓
URL String
↓
重新組成 State
只要資料跨過這個邊界,
就要重新確認:
型別有沒有改?
格式有沒有改?
空值怎麼表示?
Day 12 我們講過:
Form
↓
Request Payload
時:
undefined
null
""
可能會改變語意。
今天則是:
State
↓
URL
時:
Array
可能變成:
String
共同點都是:
資料只要跨過一個系統邊界,就不能假設它還保持原樣。
例如:
Form
creationMethodIn = ?
確認:
["FA"]
再看:
URL
creationMethodIn = ?
確認:
"FA"
再看回來後:
Search Params
creationMethodIn = ?
如果還是:
"FA"
就知道:
Deserialize 少掉了。
最後 API Payload:
creationMethodIn = ?
應該要重新回到:
["FA"]
有些 Bug 並不是:
資料內容錯了
而是:
資料型別錯了。
例如:
"FA"
跟:
["FA"]
人看起來都像:
選了 FA。
但對程式來說:
String
和:
Array
完全是不同的資料。
所以之後遇到:
400
Filter 還原失敗
URL 回來資料怪怪的
除了看值,
也要多問:
現在這個值是什麼型別?
最後我會把今天這件事記成:
資料跨邊界
↓
先懷疑型別與格式
因為:
Form
URL
API
Database
每一層都有自己的資料表示方式。
前端很重要的一個工作,
就是在這些邊界之間:
把資料轉成下一層真正需要的形狀。
下一篇可以接另一個很有代表性的 UI Debug:
下拉選單明明拿到正確資料,為什麼畫面還是先閃 ID、再變成 Name?受控 Component 到底在控制什麼?