JavaScript – DOM 與瀏覽器 API
單元:表單操作
簡介
在前端開發中,表單是使用者與網站互動的主要管道。無論是登入、註冊、搜尋或是訂單結帳,都需要透過表單收集資料、驗證資訊、最後送出給伺服器。
隨著瀏覽器提供的 DOM API 與 Browser API 越來越完整,我們不再只能靠傳統的 <form> 直接提交,而是可以在 JavaScript 中靈活地 控制表單行為、即時驗證、非同步送出,提升使用者體驗與開發效率。
本篇文章針對 表單操作 進行系統性說明,從基礎的元素取得、事件處理,到進階的 FormData、Constraint Validation API 以及常見的陷阱與最佳實踐,幫助初學者快速上手,同時提供中級開發者可直接套用於專案的實用範例。
核心概念
1. 取得與操作表單元素
在 DOM 中,表單本身與表單內的每一個控件(input、select、textarea …)都是 Node,可以透過 document.querySelector、document.forms 等方式取得。
// 取得第一個 <form> 元素
const form = document.querySelector('form');
// 取得 name 為 "email" 的 input
const emailInput = form.elements['email']; // 或 form.querySelector('[name="email"]')
// 直接透過 id 取得
const passwordInput = document.getElementById('pwd');
小技巧:
form.elements會回傳類似陣列的 HTMLFormControlsCollection,可直接以 name 取值,寫起來更直觀。
2. 表單送出與 preventDefault()
瀏覽器預設會在 <form> 送出時重新載入頁面,對於 SPA 或需要 AJAX 送出的情境,我們必須阻止預設行為。
form.addEventListener('submit', function (e) {
e.preventDefault(); // 取消預設的頁面重新載入
console.log('表單被攔截,準備自行處理');
});
提醒:若在
submit事件中忘記呼叫e.preventDefault(),表單仍會照舊送出,導致頁面閃爍或資料遺失。
3. 讀取與整理表單資料
最常見的需求是 一次性取得所有欄位的值。FormData 物件提供了便利的介面。
form.addEventListener('submit', function (e) {
e.preventDefault();
// 建立 FormData,會自動抓取表單內所有可成功序列化的欄位
const data = new FormData(form);
// 轉成普通物件(方便後續操作)
const payload = Object.fromEntries(data.entries());
console.log(payload);
// { username: "alice", email: "alice@example.com", agree: "on" }
});
註:
FormData會自動處理 檔案上傳(<input type="file">),不需要額外手動讀取檔案。
4. 客戶端即時驗證 – Constraint Validation API
HTML5 已內建 Constraint Validation,配合 checkValidity()、setCustomValidity() 可以在 JavaScript 中自行觸發或自訂錯誤訊息。
<input type="email" name="email" required id="email">
<span id="emailError" class="error-msg"></span>
const emailInput = document.getElementById('email');
const emailError = document.getElementById('emailError');
emailInput.addEventListener('input', () => {
// 先清除自訂錯誤
emailInput.setCustomValidity('');
// 使用內建驗證規則
if (!emailInput.checkValidity()) {
emailInput.setCustomValidity('請輸入有效的 Email 格式');
}
// 顯示錯誤訊息
emailError.textContent = emailInput.validationMessage;
});
要點:
validationMessage會回傳瀏覽器產生的預設訊息或setCustomValidity設定的文字,直接顯示即可。
5. 非同步送出 – fetch + FormData
將表單資料以 AJAX 方式送往後端,最常見的做法是 fetch 搭配 FormData。
form.addEventListener('submit', async function (e) {
e.preventDefault();
const data = new FormData(form);
try {
const response = await fetch('/api/register', {
method: 'POST',
body: data, // 自動設定 multipart/form-data
// 若要傳 JSON,則需要自行轉換:
// body: JSON.stringify(Object.fromEntries(data.entries())),
// headers: { 'Content-Type': 'application/json' }
});
if (!response.ok) throw new Error('伺服器回傳錯誤');
const result = await response.json();
console.log('註冊成功', result);
} catch (err) {
console.error('送出失敗', err);
}
});
說明:當
body為FormData時,瀏覽器會自動加上multipart/form-data的 boundary,開發者不需要自行設定Content-Type。
6. 動態新增/刪除表單欄位
許多應用(如商品規格、問卷)需要使用者自行 增減 表單欄位。以下示範如何在 DOM 中動態插入 <input>,並保持 name 的唯一性。
<div id="questions"></div>
<button id="addQ">新增問題</button>
let qCount = 0;
document.getElementById('addQ').addEventListener('click', () => {
qCount++;
const div = document.createElement('div');
div.className = 'question-item';
div.innerHTML = `
<label>問題 ${qCount}:</label>
<input type="text" name="question_${qCount}" required>
<button type="button" class="remove">移除</button>
`;
// 加入刪除功能
div.querySelector('.remove').addEventListener('click', () => div.remove());
document.getElementById('questions').appendChild(div);
});
注意:若要在送出時取得動態欄位的值,只要它們仍在
<form>內,new FormData(form)會自動包含。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 / 最佳實踐 |
|---|---|---|
忘記 e.preventDefault() |
表單會直接送出,導致頁面重新載入或重複請求。 | 在 submit 事件的第一行加上 e.preventDefault(),或使用 onsubmit="return false;"(不建議)。 |
直接使用 innerHTML 注入使用者輸入 |
可能產生 XSS 漏洞。 | 使用 textContent、setAttribute,或在插入前做 HTML 轉義。 |
FormData 失去檔案資訊 |
若未正確設定 <input type="file"> 的 files,FormData 只會得到空值。 |
確認 input 有 multiple 或 accept 屬性,並在送出前檢查 input.files.length。 |
| 同步驗證與非同步驗證衝突 | 同時使用 HTML5 原生驗證與自訂 AJAX 檢查(如帳號重複)時,訊息可能互相覆寫。 | 先使用 checkValidity() 判斷 HTML5 規則,再自行呼叫 API,最後再根據結果調整 setCustomValidity。 |
忘記為動態欄位加上 name |
FormData 只會收集有 name 屬性的欄位。 |
動態產生時務必同時設定唯一的 name,或在送出前手動加入至 FormData。 |
不當使用 Content-Type |
手動設定 Content-Type: multipart/form-data 會導致 boundary 錯誤。 |
不要 手動設定 Content-Type,讓瀏覽器自動處理。 |
最佳實踐總結:
- 結構化表單:使用
<fieldset>、<legend>分組,提升可讀性與可存取性(ARIA)。 - 一次性取得資料:
new FormData(form)為最簡潔且支援檔案的方式。 - 分層驗證:先由 HTML5 原生規則過濾,再由 JavaScript 做自訂檢查,最後在伺服器端再次驗證(不可省略)。
- 非同步送出:配合
async/await讓程式碼更易讀;捕捉錯誤並提供使用者友善的回饋。 - 保持可存取性:錯誤訊息使用
aria-live="polite",讓螢幕閱讀器即時讀出。
實際應用場景
| 場景 | 需求 | 可能的實作方式 |
|---|---|---|
| 使用者註冊 | 必填欄位、Email 格式驗證、密碼強度、帳號重複檢查、非同步送出 | 1. required + type="email" 2. input.addEventListener('input', checkPasswordStrength) 3. fetch('/api/check-username') 做即時驗證 |
| 商品訂購 | 多筆商品明細、即時小計、表單送出前檢查庫存 | 使用 動態欄位 產生商品列,change 事件即時計算小計,最後以 FormData + fetch 送出。 |
| 問卷調查 | 多種題型(單選、複選、文字)、可自行新增題目、結果即時預覽 | 依題型生成不同 <input>,使用 FormData 收集,JSON.stringify 後送出,伺服器回傳統計圖表。 |
| 檔案上傳 | 多檔案、進度條、檔案類型/大小限制 | input[type="file"] 設 multiple,在 change 事件中檢查 file.size、file.type,使用 XMLHttpRequest 或 fetch 搭配 onprogress 顯示進度。 |
總結
表單是前端與後端溝通的核心橋樑,掌握 DOM 操作、Browser API(如 FormData、Constraint Validation)以及 非同步送出 的技巧,能讓我們:
- 提升使用者體驗:即時驗證、無刷新送出、動態增減欄位。
- 減少程式錯誤:利用標準 API 減少手寫序列化與錯誤處理的程式碼。
- 加強安全性與可存取性:遵循 HTML5 原生驗證、ARIA 以及 XSS 防護最佳實踐。
從本文的範例出發,你可以快速在自己的專案中加入 健全的表單處理機制,無論是簡單的登入表單,或是複雜的多步驟訂購流程,都能以乾淨、可維護的程式碼完成。祝你寫程式快快樂樂,表單玩得開心!