題幹與適用場景
你要實作一個可拖曳的行動版側邊欄。桌面使用者期待 Esc 關閉,Android 使用者期待返回手勢或返回按鈕關閉;表單有未儲存內容時必須先確認。請使用 CloseWatcher 設計統一關閉流程,並說明 cancel、close、requestClose()、destroy()、焦點、多個 watcher、瀏覽器不支援和歷史回退邊界。
MDN 將 CloseWatcher 描述為讓自訂元件回應裝置特定關閉操作的介面。HTML Standard 也規定了 close watcher 的分組和防止濫用歷史操作的邊界。本文根據公開資料整理,不聲稱是公司真題。
面試官考察點
面試官關注你是否把「關閉請求」和「立即關閉」區分開,能否在 cancel 階段阻止關閉並展示確認,同時在唯一的 close 處理器中清理 UI。強回答會提到使用者啟用、多個 watcher 的分組、AbortSignal 生命週期、焦點返回和不支援時的普通按鈕回退;普通回答只監聽 keydown。
回答前需要釐清的問題
- 側邊欄是模態、非模態,還是與頁面歷史綁定的導覽狀態?
- 未儲存內容需要阻止所有關閉請求,還是只在特定欄位變髒時阻止?
- 返回手勢是關閉目前元件,還是應該返回上一筆歷史紀錄?
- 目標瀏覽器是否支援 CloseWatcher,是否允許關閉能力退化為明確按鈕?
30 秒回答框架
「我把所有關閉入口統一為 close request。開啟側邊欄時建立一個帶 AbortSignal 的 CloseWatcher,cancel 檢查表單是否變髒;變髒時阻止預設關閉並顯示確認,乾淨時讓流程繼續。close 負責隱藏元件、恢復焦點和銷毀資源,明確的關閉按鈕也走同一套邏輯。能力不支援時保留按鈕和 Esc 監聽,返回手勢交給瀏覽器或歷史處理,不能阻斷核心導覽。」
分步驟深入解答
先區分兩個動作:requestClose() 模擬裝置關閉請求,會先觸發 cancel,沒有被阻止才觸發 close;close() 立即觸發 close,跳過 cancel;destroy() 只停用 watcher。儲存成功、元件卸載或路由離開時,應明確選擇請求關閉還是強制清理,不能混用兩個語意。
function openDrawer() {
const controller = new AbortController();
const watcher = new CloseWatcher({ signal: controller.signal });
watcher.addEventListener("cancel", (event) => {
if (!formIsDirty()) return;
event.preventDefault();
showDiscardConfirmation(() => watcher.close());
});
watcher.addEventListener("close", () => {
hideDrawer();
restoreFocusToTrigger();
controller.abort();
});
return { watcher, controller };
}確認對話框本身不能遞迴製造無法關閉的 watcher。確認放棄時可以呼叫目前 watcher 的 close(),因為使用者已完成明確確認;取消確認則保持側邊欄開啟。儲存成功後清除變髒狀態,再呼叫 requestClose(),讓同一套清理路徑繼續執行。
焦點策略取決於元件類型。開啟模態側邊欄時把焦點放到可解釋的標題或第一個控制項,關閉後返回觸發按鈕;非模態面板不應強行搶焦點,但仍要有可達的關閉按鈕和可見狀態。關閉請求不能只改變 CSS,還要更新可存取名稱、遮罩、捲動鎖和鍵盤順序。
多個 watcher 有特殊邊界。沒有使用者啟用建立多個 watcher 時,規範允許它們被分組,一次 close request 可能同時關閉多個 watcher;不要為每個小彈層都無條件建立實例。優先讓一個頂層元件擁有 watcher,子元件透過事件請求父元件關閉,並在卸載時呼叫 destroy() 或中止關聯訊號。
返回手勢不能簡單當成普通點擊。平台可能把返回動作作為歷史遍歷或 close request,瀏覽器會依據 close watcher 管理器決定目標。應用程式只在確實能攔截且元件處於開啟狀態時執行確認;若沒有可關閉元件,應讓瀏覽器繼續歷史返回。不要全域阻止 popstate 或返回手勢來保護一個區域表單。
能力偵測和回退要保住核心路徑。檢查 window.CloseWatcher 後再建立實例;不支援時保留明確關閉按鈕,必要時增加受限的 Esc 監聽,並把返回手勢交給既有路由。不要宣稱自訂監聽能完全複製 Android 返回行為。記錄支援率、關閉請求阻止率、確認後放棄率和焦點回退錯誤。
高品質示範回答
我會把側邊欄的所有關閉入口統一為 close request。元件開啟時建立帶 AbortSignal 的 CloseWatcher,cancel 只負責判斷未儲存狀態:變髒時 preventDefault() 並顯示確認,乾淨時不阻止。確認放棄後呼叫 close(),儲存成功後清除變髒狀態並呼叫 requestClose()。close 是唯一的 UI 清理點,負責隱藏面板、恢復觸發按鈕焦點、解除捲動鎖並終止 watcher。
我會限制 watcher 數量,避免未經使用者啟用建立的多個實例被分組關閉;元件卸載或路由切換時銷毀它。模態和非模態元件分別處理焦點和遮罩,沒有開啟元件時讓返回手勢交給瀏覽器歷史。沒有 CloseWatcher 的瀏覽器保留明確按鈕和有限的鍵盤回退,不阻斷核心導覽,並用指標驗證關閉、確認和可存取性行為。
常見錯誤
- 錯誤表現 → 用
close()處理所有入口;失敗原因 → 跳過了未儲存確認的cancel階段;修正方法 → 使用者請求走requestClose(),強制清理才用close()。 - 錯誤表現 → 只監聽 Esc;失敗原因 → 無法涵蓋 Android 返回手勢和其他裝置關閉動作;修正方法 → 使用 CloseWatcher,並保留明確按鈕。
- 錯誤表現 → 每個子彈層都建立 watcher;失敗原因 → 未啟用建立的 watcher 可能被分組關閉;修正方法 → 由頂層可關閉元件統一擁有實例。
- 錯誤表現 → 關閉後焦點留在已隱藏節點;失敗原因 → 鍵盤和輔助技術失去当前位置;修正方法 → 記錄觸發器並在
close中恢復焦點。 - 錯誤表現 → 全域阻止返回事件;失敗原因 → 破壞沒有開啟元件時的歷史導覽;修正方法 → 只在開啟且確需確認時阻止關閉請求。
追問及應對
requestClose() 和 close() 什麼時候分別使用?
使用者或平台發起的關閉意圖使用 requestClose(),它給 cancel 一個阻止機會;使用者確認放棄、元件已卸載或清理流程必須立即結束時使用 close()。兩者最終都應匯入同一個 close 清理處理器。
未儲存確認對話框也要建立 CloseWatcher 嗎?
不一定。確認對話框應有自己的明確關閉按鈕和可存取焦點路徑,避免與父 watcher 形成遞迴或分組關閉。父 watcher 在確認完成後執行 close(),子對話框關閉只改變確認狀態。
如何處理多個開啟的面板?
定義堆疊或頂層所有權:一次關閉請求只關閉最上層、確實可關閉的元件,其餘保持開啟。不要依賴多個未經使用者啟用 watcher 的隱式分組行為實作業務堆疊,應用程式層要記錄順序和返回焦點。
CloseWatcher 不支援時怎樣回退?
保留明確關閉按鈕和基礎 Esc 處理,繼續執行同一份變髒狀態確認、焦點恢復和清理函式。返回手勢及歷史導覽不做全域攔截;透過能力分組監控退化比例,逐步決定是否擴大增強範圍。