題目與適用情境
為一個結帳表單設計驗證流程。欄位包括電子郵件、國家、郵遞區號、配送日期和可選優惠碼。 郵遞區號規則隨國家變化,優惠碼需要遠端驗證。價格、庫存、地址規則和優惠碼有效性最終都由 伺服器判定。
表單必須支援鍵盤和輔助技術,在 200% 縮放以及伺服器往返後仍可用。錯誤不能只靠顏色表達。 提交失敗後必須保留已有輸入,指出全部可操作錯誤,並提供直接的修正路徑。網路失敗、優惠碼 過期,以及舊非同步回應晚於新回應返回,都屬於題目範圍。
目標不是發明表單函式庫,而是定義狀態、驗證時機、語意關係、焦點策略和前後端錯誤契約,再 說明如何驗證這些決策。原生 HTML 約束是基礎;自訂行為只能增強清晰度,不能移除瀏覽器語意。
面試官考察什麼
第一是職責邊界。用戶端驗證用於快速回饋和減少無效請求,伺服器驗證決定訂單是否成立。只信任 用戶端的 required 或折扣金額,會留下安全與正確性漏洞。
第二是復原能力。無效邊框不夠。每個錯誤都需要文字、與控制項的程式化關聯和修正說明。提交時, 使用者還需要總覽和可預期的焦點位置,已經正確填寫的內容不能遺失。
第三是時機。使用者仍在輸入電子郵件時立即顯示「無效」,會製造噪音。第一次提交時統一驗證; 欄位已經失敗後,再在失焦或有意義的變更時複驗,讓使用者知道修正是否生效。非同步驗證還要有 等待狀態和過期回應保護。
最後是驗證深度。自動化無障礙掃描能發現標籤缺失,卻不能證明播報是否合理、焦點順序是否正確、 伺服器返回後能否復原,以及亂序優惠碼回應是否會覆蓋新值。這些都需要互動測試。
回答前應釐清的問題
- 哪些驗證可在本機完成? 必填、格式和簡單跨欄位規則可在本機執行;價格、庫存、資格和
最終優惠有效性屬於伺服器。
- 何時顯示回饋? 提交時顯示全部阻斷錯誤。已經失敗的欄位可在失焦或明確修改後複驗;不要
每次按鍵都播報。
- 伺服器會重新渲染頁面嗎? 增強提交和普通表單提交都必須保留輸入並渲染同一套結構化錯誤。
- 一個錯誤會不會涉及多個控制項? 國家與郵遞區號共享一條規則。共享說明放在欄位群組附近,
摘要連結指向開始修正的控制項。
- 優惠碼驗證期間如何處理? 等待是資訊狀態,不是錯誤。目前值和請求代次共同決定回應是否
仍有效。
- 失敗後焦點移到哪裡? 多個錯誤時聚焦表單前的錯誤摘要;緊湊表單只有一個錯誤時,可聚焦
對應控制項。應選定一種策略,避免同一變化重複播報。
- 哪些失敗不是欄位錯誤? 網路故障或庫存變化屬於表單層級訊息,並應提供重試路徑,不能掛在
電子郵件或郵遞區號下。
- 流程是否會暴露敏感事實? 登入與帳號復原可能需要通用伺服器訊息;修正建議不能洩露帳號
是否存在。
30 秒回答框架
「我先使用帶標籤的原生控制項和約束,但最終以伺服器驗證為準。每個無效控制項設定 aria-invalid="true",並用 aria-describedby 關聯穩定的文字錯誤;顏色和圖示只做輔助。 第一次失敗提交會在表單前渲染帶欄位連結的錯誤摘要,並把焦點移到摘要,已有輸入保持不變。
首次在提交時驗證,失敗過的欄位再在失焦或有意義的修改時複驗。優惠碼請求會防抖、可取消,並 攜帶代次,舊回應不能覆蓋新值。伺服器以結構化格式返回已知欄位錯誤和表單層級錯誤。除自動化 檢查外,我還會測試鍵盤和讀屏復原、縮放與強制顏色、伺服器往返、停用 JavaScript 的提交,以及 非同步競態。」
分步深入設計
第一步:定義狀態與不變量
把欄位值與驗證狀態分開。每個欄位可處於未觸碰、等待、有效或無效狀態,錯誤碼映射為本地化 文字;表單層級錯誤單獨保存。記錄是否已經嘗試提交,避免把輸入到一半的欄位直接標成失敗。
不變量包括:標籤始終可見;說明先於錯誤存在;每條可見欄位錯誤都有程式化關聯;隱藏錯誤不被 引用;一次提交只有一次明確焦點移動;驗證失敗不會清空正確輸入;過期非同步回應不能修改目前狀態。
第二步:先建立語意控制項,再補 ARIA
使用 label、最準確的輸入類型、required、autocomplete 和合適的長度或模式約束。國家相關 郵遞區號規則可以用 setCustomValidity()。值有效後必須用空字串清除自訂錯誤,否則瀏覽器仍會 阻止提交。
<label for="email">Email</label>
<input id="email" name="email" type="email"
aria-invalid="true"
aria-describedby="email-hint email-error">
<p id="email-hint">name@example.com</p>
<p id="email-error">Enter a valid email address.</p>checkValidity() 只檢查約束,reportValidity() 還會要求瀏覽器顯示回饋。產品若渲染自己的 無障礙錯誤訊息,就應一致地接管無效狀態,避免同時出現兩套互相競爭的錯誤系統。
第三步:有意選擇驗證時機
第一次提交時驗證全部欄位並顯示所有阻斷錯誤。此後,無效欄位在失焦時複驗。對選擇項或形態 已經完整且無歧義的修正值,可以在變更後更快清除錯誤。日期或電子郵件尚未輸入完整時,不要讓 live region 反覆播報。
跨欄位規則在任一依賴變化時執行。國家變化後要重新驗證郵遞區號並更新可見說明。除非正規化 安全且可逆,否則不要悄悄改寫輸入;去掉首尾空白與猜測郵遞區號格式是兩種不同操作。
第四步:同時提供行內錯誤與可導覽摘要
在欄位附近渲染簡短文字,僅在無效時設定 aria-invalid,並引用錯誤的穩定 ID。視覺樣式在強制 顏色模式下也要可辨識,而且必須包含文字,不能只有紅框或警告圖示。
多錯誤提交失敗後,在表單前插入帶標題的摘要。為其提供臨時可聚焦能力,只聚焦一次,說明錯誤 數量,並把每項連結到對應控制項。連結文字應包含欄位名和修正方式。同一事件中不要又把焦點移到 首個錯誤欄位,否則使用者很難檢查摘要。
第五步:讓非同步驗證抵抗競態
優惠碼形態完整後再防抖發起請求。值變化時取消上一個請求,完成時還要比較單調遞增的請求代次。 兩層保護都需要,因為取消發生時,回應可能已經進入後續處理。
透過禮貌級 live region 在欄位旁顯示「正在檢查」。等待期間停用套用優惠動作,但不要停用無關 欄位。逾時應按產品契約成為可重試的表單層級或優惠碼狀態,不能說成「優惠碼無效」。快取結果 必須包含使它成立的價格情境和過期時間。
第六步:協調伺服器權威錯誤
透過真實表單 action 或增強請求提交原始值。伺服器再次正規化和驗證、計算總價,並返回欄位 錯誤碼、表單錯誤碼和接收值等結構。用戶端只映射白名單欄位名。未知鍵轉為安全的表單層級錯誤, 不能直接變成選擇器或標記內容。
拒絕時保留全部非敏感輸入,用伺服器事實替換用戶端推測,渲染同一錯誤摘要並聚焦。成功時顯示 明確確認,並防止重複觸發。如果提交後的回應遺失,應用冪等鍵或先查詢訂單,不能直接鼓勵使用者 再次扣款。
第七步:驗證復原路徑,而不只比較快照
測試空提交、單錯誤、多錯誤、國家與郵遞區號依賴、過期配送日期、優惠碼逾時、無效優惠碼、快速 切換優惠碼、僅伺服器發現的錯誤,以及成功回應遺失。斷言輸入仍在、摘要連結聚焦正確控制項、修正 後錯誤消失、舊非同步回應被忽略。
再用純鍵盤和讀屏完整操作流程,檢查 200% 縮放、重排、可見焦點、強制顏色、瀏覽器自動填入, 以及沒有用戶端 JavaScript 時的普通伺服器提交。自動化無障礙和單元測試只能補充,不能替代播報 與焦點測試。
高品質示範回答
「我會把欄位值與 touched、pending 和 error 狀態分開。原生標籤、輸入類型、自動填入 token 和 約束提供基礎。跨欄位自訂規則使用 setCustomValidity(),通過時一定清空訊息。用戶端驗證改善 回饋速度,伺服器在接單前重新驗證價格、庫存、地址和優惠。
第一次提交會顯示全部阻斷錯誤。每個無效輸入設定 aria-invalid="true",並透過 aria-describedby 引用可見修正文字。多個錯誤時,我在表單前渲染摘要,只聚焦一次,並把每項 連結到控制項。正確值保持不變。此後失敗欄位在失焦或有意義的修改時複驗,不會每次按鍵都觸發播報。
優惠碼驗證經過防抖,同時攜帶取消訊號和代次。只有符合目前值的回應可以更新頁面。等待、不可用 與無效是三種狀態。伺服器欄位碼經白名單映射,全域故障留在表單層級。自動化檢查通過後,還必須 完成鍵盤、讀屏、縮放、無腳本伺服器往返、競態和重複提交測試。」
常見錯誤
- 只顯示紅框 → 部分使用者無法感知狀態 → 增加修正文字、程式化關聯和非顏色提示。
- 每次按鍵都驗證 → 未完成輸入不斷製造噪音 → 提交時驗證,之後在合適邊界複驗失敗欄位。
- 沒有清空
setCustomValidity()→ 修正後的輸入仍無效 → 規則通過時設為空字串。 - 同時聚焦摘要與首個欄位 → 播報和焦點互相爭搶 → 只做一次明確焦點移動,用連結導覽。
- 把逾時當作無效優惠碼 → 基礎設施故障變成錯誤歸責 → 分別表達等待、不可用和無效。
- 接受最後返回的回應 → 過期驗證覆蓋目前輸入 → 取消舊任務並比較請求代次。
- 信任用戶端總價 → 請求可繞過介面 → 伺服器重新計算並驗證。
- 失敗後清空表單 → 錯誤復原變成重新輸入 → 保留有效非敏感值,只更新錯誤狀態。
追問與回答
追問一:錯誤摘要應使用 role="alert" 嗎?
動態插入的錯誤可以透過 alert 或合適的 live region 播報,焦點也能讓使用者讀到它。焦點與強提醒 同時使用可能重複播報,必須測試選定組合。整頁伺服器跳轉時,把錯誤數量放入頁面標題和主標題, 能更早提供情境。
追問二:為什麼不在表單有效前停用提交按鈕?
停用按鈕可能隱藏剩餘問題,在某些導覽方式下也無法到達。保留提交路徑,讓使用者主動獲得完整 驗證結果。只在真實提交進行中暫時停用,說明狀態,並確保失敗後可復原。
追問三:何時應使用 aria-describedby,何時使用 live region?
aria-describedby 讓使用者到達欄位時獲得目前說明與錯誤;live region 在不移動焦點時播報有意義 的動態變化。靜態行內錯誤無需全部成為 live region,否則會一次播報過多內容。批次錯誤使用摘要, 真正非同步的狀態使用禮貌播報。
追問四:如何在沒有 JavaScript 時支援伺服器驗證?
使用真實表單 action。伺服器返回同一頁面,保留輸入和欄位錯誤碼,在表單前渲染摘要,並把錯誤 數放進標題或主標題。摘要連結使用穩定控制項 ID。用戶端增強也消費同一錯誤契約,避免兩種模式漂移。
追問五:如何確定性測試非同步競態?
控制兩個驗證 Promise。先啟動請求 A,修改值後啟動 B;先讓 B 返回有效,再讓 A 返回無效。斷言 頁面仍顯示 B 和目前值的結果。再覆蓋取消、逾時、元件卸載和等待期間重新提交。