本文 AI 產出,尚未審核

JavaScript – DOM 與瀏覽器 API

單元:表單操作


簡介

在前端開發中,表單是使用者與網站互動的主要管道。無論是登入、註冊、搜尋或是訂單結帳,都需要透過表單收集資料、驗證資訊、最後送出給伺服器。
隨著瀏覽器提供的 DOM APIBrowser API 越來越完整,我們不再只能靠傳統的 <form> 直接提交,而是可以在 JavaScript 中靈活地 控制表單行為、即時驗證、非同步送出,提升使用者體驗與開發效率。

本篇文章針對 表單操作 進行系統性說明,從基礎的元素取得、事件處理,到進階的 FormData、Constraint Validation API 以及常見的陷阱與最佳實踐,幫助初學者快速上手,同時提供中級開發者可直接套用於專案的實用範例。


核心概念

1. 取得與操作表單元素

在 DOM 中,表單本身與表單內的每一個控件(inputselecttextarea …)都是 Node,可以透過 document.querySelectordocument.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);
  }
});

說明:當 bodyFormData 時,瀏覽器會自動加上 multipart/form-databoundary,開發者不需要自行設定 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 漏洞。 使用 textContentsetAttribute,或在插入前做 HTML 轉義
FormData 失去檔案資訊 若未正確設定 <input type="file">filesFormData 只會得到空值。 確認 inputmultipleaccept 屬性,並在送出前檢查 input.files.length
同步驗證與非同步驗證衝突 同時使用 HTML5 原生驗證與自訂 AJAX 檢查(如帳號重複)時,訊息可能互相覆寫。 先使用 checkValidity() 判斷 HTML5 規則,再自行呼叫 API,最後再根據結果調整 setCustomValidity
忘記為動態欄位加上 name FormData 只會收集有 name 屬性的欄位。 動態產生時務必同時設定唯一的 name,或在送出前手動加入至 FormData
不當使用 Content-Type 手動設定 Content-Type: multipart/form-data 會導致 boundary 錯誤。 不要 手動設定 Content-Type,讓瀏覽器自動處理。

最佳實踐總結

  1. 結構化表單:使用 <fieldset><legend> 分組,提升可讀性與可存取性(ARIA)。
  2. 一次性取得資料new FormData(form) 為最簡潔且支援檔案的方式。
  3. 分層驗證:先由 HTML5 原生規則過濾,再由 JavaScript 做自訂檢查,最後在伺服器端再次驗證(不可省略)。
  4. 非同步送出:配合 async/await 讓程式碼更易讀;捕捉錯誤並提供使用者友善的回饋。
  5. 保持可存取性:錯誤訊息使用 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.sizefile.type,使用 XMLHttpRequestfetch 搭配 onprogress 顯示進度。

總結

表單是前端與後端溝通的核心橋樑,掌握 DOM 操作Browser API(如 FormData、Constraint Validation)以及 非同步送出 的技巧,能讓我們:

  • 提升使用者體驗:即時驗證、無刷新送出、動態增減欄位。
  • 減少程式錯誤:利用標準 API 減少手寫序列化與錯誤處理的程式碼。
  • 加強安全性與可存取性:遵循 HTML5 原生驗證、ARIA 以及 XSS 防護最佳實踐。

從本文的範例出發,你可以快速在自己的專案中加入 健全的表單處理機制,無論是簡單的登入表單,或是複雜的多步驟訂購流程,都能以乾淨、可維護的程式碼完成。祝你寫程式快快樂樂,表單玩得開心!