iT邦幫忙

2026 iThome 鐵人賽

DAY 21
0
佛心分享-IT 人職涯歷練

從 UI/UX 麻瓜到工程魔法師|從讀懂程式開始系列 第 21 篇

Day 21|多選明明是 Array,為什麼放進 URL 再回來後,竟然變成 String?

  • 分享至 

  • xImage
  •  

前幾天我們一直在練:

Request 為什麼多打?
為什麼沒重新打?
Cache 到底在哪一層影響資料?

今天換另一種很常見、而且非常容易讓人誤判的問題:

原本明明是 Array,為什麼進 URL 繞一圈回來,竟然變成 String?

這種 Bug 表面上看起來像:

API 型別錯了

但真正的問題可能發生得更早。

是在:

Form
↓
URL
↓
再還原回 Form / Request

這條路上。


先看一個簡單的多選條件

假設使用者選:

["FA", "PA"]

這是一個很正常的 Array。

例如:

const creationMethodIn = ["FA", "PA"];

如果直接送 API,而 Backend 也期待 Array:

{
  "creationMethodIn": ["FA", "PA"]
}

完全沒問題。


但 URL 本身不是拿來直接存 Array 的

例如我們想把搜尋條件同步到網址:

?page=1&creationMethodIn=FA,PA

這時 URL 裡面的:

FA,PA

其實只是:

String

不是 JavaScript Array。

也就是:

["FA", "PA"]

進 URL 之後,可能變成:

"FA,PA"

資料型別已經改變。


問題通常不是「寫進 URL」

把 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 很容易只在:

單選一個選項

時發生。


這就是 Serialize / Deserialize

這裡其實是在做兩件事。

寫進 URL

Array
↓
String

這叫做:

Serialize

例如:

["FA", "PA"]

轉成:

"FA,PA"

可以寫:

value.join(",");

從 URL 讀回來

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 還原後也正常。


這也是為什麼 Debug 要測完整使用流程

如果只測:

進列表
↓
選 Filter
↓
Search

可能完全正常。

但真正的 Bug 流程是:

選 Filter
↓
進 Detail
↓
Back
↓
Search Params 從 URL 還原
↓
API 400

所以測試不能只看:

這個 Component 當下能不能操作。

還要看:

資料離開這個 Component 之後,繞一圈回來還是不是原本的型別。


可以集中處理這種 Array 欄位

假設有:

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"]

寫回 URL 時則反過來

假設:

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 想成:

就是一個網址。

但做搜尋條件同步後,才發現:

URL 也是資料儲存與傳遞的一層。

例如:

React State
↓
URL String
↓
重新載入
↓
URL String
↓
重新組成 State

只要資料跨過這個邊界,

就要重新確認:

型別有沒有改?

格式有沒有改?

空值怎麼表示?

這跟 API Payload 其實很像

Day 12 我們講過:

Form
↓
Request Payload

時:

undefined
null
""

可能會改變語意。

今天則是:

State
↓
URL

時:

Array

可能變成:

String

共同點都是:

資料只要跨過一個系統邊界,就不能假設它還保持原樣。


Debug 時我現在會追「每一站的型別」

例如:

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 到底在控制什麼?


上一篇
Day 20|明明有 reload(),為什麼資料就是不重新抓?Cache、Request Key 到底在幫忙還是在搗亂?
下一篇
Day 22|下拉選單為什麼先閃 ID、再變成 Name?受控 Component 到底在控制什麼?
系列文
從 UI/UX 麻瓜到工程魔法師|從讀懂程式開始 共 26 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言