FormData 欄位不見了?檢查 disabled、name 與 submitter
按下儲存後,程式先把整份表單 disabled,避免連點,再呼叫 new FormData(form);結果送到後端的欄位比畫面少。FormData 不是把所有可見 input 都收進去:先確認控制項的 name、停用狀態與選取狀態,並在鎖定欄位之前建立資料快照。
有「儲存草稿」與「發布」兩顆按鈕的表單,還要把實際觸發送出的 event.submitter 傳給建構子,才能保留那顆按鈕的名稱和值。資料本身有沒有收齊,應先在請求送出前確認,再往後端 parser 排錯。
從收集條件找缺少的欄位
WHATWG 的表單 entry list 演算法定義哪些控制項會被收集。幾個容易誤判的情況如下:
| 表單狀態 | FormData 的結果 | 要檢查什麼 |
|---|---|---|
| 只有 id,沒有 name | 不會得到預期名稱的欄位 | id 用於 DOM 識別,資料 key 要用 name |
| input disabled | 不收集該控制項 | 是否在建立快照前鎖定介面 |
| 支援 readonly 的 input | 仍可收集 | readonly 限制編輯,不等同 disabled |
| checkbox 未勾選 | 不會自動送出 false | 後端需明確定義缺少欄位的語意 |
| 多個相同 name | 可以保留多筆值 | 使用 getAll,不要只讀 get |
| submit button | 依指定的 submitter 收集 | 確認當次點按的是哪個按鈕 |
disabled 的 <fieldset> 也會影響其子控制項,首個 <legend> 的後代有規格例外;排錯時要檢查祖先,不只看 input 自己有沒有 disabled attribute。相反地,用 CSS 隱藏不等於停用,type="hidden" 也可以提交資料。畫面是否看得見並不是可靠的收集條件。
readonly 不是每種控制項都支援,不能把它當成 select 或 checkbox 的通用替代方法。若欄位必須禁止操作但仍要提交,請先設計資料來源:在鎖定前建立快照,或由可信的應用狀態加入該值。伺服器仍須驗證欄位,不應因為前端 readonly 就信任它。
可重現的表單:先看快照,再看是哪顆按鈕
以下存成 formdata-check.html,使用目標瀏覽器開啟。它不會發出網路請求;按「儲存」、「發布」和最後一個檢查按鈕,比較輸出即可。
<!doctype html><html lang="zh-Hant"><meta charset="utf-8"><title>FormData 欄位檢查</title><form id="profile"> <label>名稱 <input name="displayName" value="Zero"></label> <label>停用 <input name="disabledField" value="missing" disabled></label> <label>唯讀 <input name="readOnlyField" value="included" readonly></label> <label>只有 id <input id="unnamed" value="missing"></label> <label><input type="checkbox" name="newsletter" value="yes">訂閱</label> <label><input type="checkbox" name="topics" value="css" checked>CSS</label> <label><input type="checkbox" name="topics" value="js" checked>JavaScript</label> <button type="submit" name="intent" value="save">儲存</button> <button type="submit" name="intent" value="publish">發布</button></form><button type="button" id="inspect">省略 submitter 的快照</button><pre id="result"></pre><script> const form = document.getElementById('profile'); const result = document.getElementById('result');
function show(data) { result.textContent = JSON.stringify({ entries: [...data], topics: data.getAll('topics'), flattenedTopics: Object.fromEntries(data).topics, }, null, 2); }
form.addEventListener('submit', event => { event.preventDefault(); show(new FormData(form, event.submitter)); }); document.getElementById('inspect').addEventListener('click', () => { show(new FormData(form)); });</script></html>依規格,未改動範例時,disabledField、沒有 name 的 input 和未勾選的 newsletter 不會出現;readOnlyField 會被收集。點按儲存應有 intent=save,發布應有 intent=publish,省略 submitter 的快照則沒有這兩顆按鈕的 intent。
這些是供你對照的預期值。本次內容維護已檢閱範例與檢查 JavaScript 語法,沒有取得瀏覽器實測結果,也沒有測試舊瀏覽器對建構子第二個參數的相容性。正式專案請在自己的支援矩陣驗收;MDN 的 FormData 建構子頁面有第二個參數與例外條件的說明。
多值欄位別直接轉成普通物件
上例 topics 有 css、js 兩筆;data.getAll('topics') 保留兩者,Object.fromEntries(data) 則讓後面的同名 key 覆蓋前面。因此把整份 FormData 轉成 JSON 時,看到少了一個選項,問題可能出在轉換,而非表單收集。
要送 JSON,依欄位 schema 明確建構:
const data = new FormData(form, submitter);const payload = { displayName: data.get('displayName'), topics: data.getAll('topics'), newsletter: data.has('newsletter'), intent: data.get('intent'),};這段是 schema 轉換示意,form、submitter 取自你的 submit handler;只有當所有選項都已載入、欄位確實可操作時,缺少 newsletter 才能解讀為未勾選。若控制項被停用或根本沒有渲染,缺少 key 也可能代表「不修改」;PATCH API 尤其應先把語意定義好。檔案則應另外處理,不能假設把 File 放進 JSON 就會上傳檔案內容。
先建立快照,再停用送出按鈕
下面是接到產品 API 的整合示意,需要替換 /api/profile 及回應處理。它只停用 submit button,並且保留原本就停用的狀態,避免結束後把不能操作的按鈕一律打開:
let sending = false;form.addEventListener('submit', async event => { event.preventDefault(); if (sending) return;
const data = new FormData(form, event.submitter); const buttons = [...form.querySelectorAll('button[type="submit"]')]; const states = buttons.map(button => button.disabled); sending = true; buttons.forEach(button => { button.disabled = true; });
try { const response = await fetch('/api/profile', { method: 'POST', body: data, }); if (!response.ok) throw new Error('HTTP ' + response.status); // 依專案回應格式處理成功狀態。 } catch (error) { console.error(error); // 正式產品應在表單顯示可理解的錯誤與重試方式。 } finally { buttons.forEach((button, index) => { button.disabled = states[index]; }); sending = false; }});範例限定在表單內、明確帶 type="submit" 的 button;外部關聯按鈕、input[type="submit"]、元件卸載與伺服器冪等性需要依產品補上。建立 FormData 只取得當下快照,後續 input 改值不會自動更新那份資料;若產品允許送出期間繼續編輯,要讓成功提示清楚指向哪次快照。
使用 body: data 傳送 FormData 時,MDN 明確提醒不要手動設定 multipart Content-Type,讓瀏覽器產生與本文相符的 boundary。若仍少欄位,依序比較建立快照時的 entries、Network 中的 Form Data,再比較後端解析結果。這樣能定位資料在哪一層消失。
若後端返回的本文又無法解析成 JSON,可接著看 fetch JSON 的 Unexpected token 排錯;先確認送出資料正確,再處理回應格式,兩邊不要共用一個模糊的「表單壞了」錯誤。
參考資料: