Vue refs 和生命週期方法用於焦點管理
我們幾乎完成了 Vue 的學習。最後一個要關注的功能是焦點管理,換句話說,是如何改善我們應用程式的鍵盤可訪問性。我們將研究使用 **Vue refs** 來處理它——這是一個高階功能,它允許您直接訪問虛擬 DOM 下面的底層 DOM 節點,或者從一個元件直接訪問子元件的內部 DOM 結構。
| 先決條件 |
熟悉核心 HTML、CSS 和 JavaScript 語言,瞭解 終端/命令列。 Vue 元件是用管理應用程式資料的 JavaScript 物件和對映到底層 DOM 結構的基於 HTML 的模板語法組合而成的。為了安裝,並使用 Vue 的一些更高階功能(例如單檔案元件或渲染函式),您需要一個安裝了 node + npm 的終端。 |
|---|---|
| 目標 | 學習如何使用 Vue refs 處理焦點管理。 |
焦點管理問題
雖然我們已經有了可用的編輯功能,但我們並沒有為非滑鼠使用者提供良好的體驗。具體來說,當用戶啟用“編輯”按鈕時,我們會從 DOM 中刪除“編輯”按鈕,但我們不會將使用者的焦點移動到任何地方,因此它實際上消失了。這對鍵盤和非視覺使用者來說可能會令人困惑。
瞭解當前發生的情況
- 重新載入頁面,然後按 Tab 鍵。您應該在新增新待辦事項的輸入框上看到焦點輪廓。
- 再次按 Tab 鍵。焦點應該移動到“新增”按鈕。
- 再次點選它,它將出現在第一個複選框上。再按一次,焦點應該出現在第一個“編輯”按鈕上。
- 透過按 Enter 鍵啟用“編輯”按鈕。複選框將被我們的編輯元件替換,但焦點輪廓將消失。
這種行為可能會讓人感到突兀。此外,當您再次按 Tab 鍵時會發生什麼取決於您使用的瀏覽器。同樣,如果您儲存或取消編輯,當您返回非編輯檢視時,焦點也會再次消失。
為了給使用者提供更好的體驗,我們將新增程式碼來控制焦點,以便在顯示編輯表單時將其設定為編輯欄位。我們還想在使用者取消或儲存編輯時將焦點放回“編輯”按鈕。為了設定焦點,我們需要更多地瞭解 Vue 的內部工作原理。
虛擬 DOM 和 refs
Vue 與其他一些框架一樣,使用虛擬 DOM (VDOM) 來管理元素。這意味著 Vue 在記憶體中保留了我們應用程式中所有節點的表示。任何更新首先在記憶體中的節點上執行,然後對頁面上實際節點需要進行的所有更改都會在一個批次中同步。
由於讀取和寫入實際 DOM 節點通常比虛擬節點更昂貴,這可以帶來更好的效能。但是,這也意味著在使用框架時,您通常不應該透過本機瀏覽器 API(如 Document.getElementById)直接編輯 HTML 元素,因為它會導致 VDOM 和實際 DOM 不同步。
相反,如果您需要訪問底層 DOM 節點(例如設定焦點),您可以使用 Vue refs。對於自定義 Vue 元件,您也可以使用 refs 直接訪問子元件的內部結構,但這應該謹慎使用,因為它會使程式碼難以理解和推理。
要在元件中使用 ref,您需要在要訪問的元素上新增一個 ref 屬性,並使用字串識別符號作為屬性的值。重要的是要注意,ref 在一個元件內必須是唯一的。在同一時間渲染的兩個元素不應該具有相同的 ref。
向我們的應用程式新增 ref
因此,讓我們在 ToDoItem.vue 中的“編輯”按鈕上附加一個 ref。像這樣更新它
<button
type="button"
class="btn"
ref="editButton"
@click="toggleToItemEditForm">
Edit
<span class="visually-hidden">{{label}}</span>
</button>
為了訪問與我們的 ref 關聯的值,我們使用元件例項提供的 $refs 屬性。為了檢視當我們點選“編輯”按鈕時 ref 的值,在我們的 toggleToItemEditForm() 方法中新增一個 console.log(),如下所示
toggleToItemEditForm() {
console.log(this.$refs.editButton);
this.isEditing = true;
}
如果您此時啟用“編輯”按鈕,您應該在控制檯中看到一個 HTML <button> 元素的引用。
Vue 的 $nextTick() 方法
我們希望在使用者儲存或取消編輯時將焦點設定為“編輯”按鈕。為此,我們需要在 ToDoItem 元件的 itemEdited() 和 editCancelled() 方法中處理焦點。
為了方便起見,建立一個不帶任何引數的新方法,名為 focusOnEditButton()。在其中,將您的 ref 分配給一個變數,然後呼叫 ref 上的 focus() 方法。
focusOnEditButton() {
const editButtonRef = this.$refs.editButton;
editButtonRef.focus();
}
接下來,在 itemEdited() 和 editCancelled() 方法的末尾新增對 this.focusOnEditButton() 的呼叫
itemEdited(newItemName) {
this.$emit("item-edited", newItemName);
this.isEditing = false;
this.focusOnEditButton();
},
editCancelled() {
this.isEditing = false;
this.focusOnEditButton();
},
嘗試透過鍵盤編輯,然後儲存/取消待辦事項。您會注意到焦點沒有設定,因此我們仍然需要解決問題。如果您開啟控制檯,您會看到一個錯誤訊息,內容類似於“無法訪問屬性“focus”,“editButtonRef”未定義”。這似乎很奇怪。當您啟用“編輯”按鈕時,您的按鈕 ref 已定義,但現在它不存在了。發生了什麼?
好吧,請記住,當我們將 isEditing 設定為 true 時,我們不再渲染包含“編輯”按鈕的元件部分。這意味著沒有元素可以繫結 ref,因此它變為 undefined。
您現在可能在想“嘿,我們不是在嘗試訪問 ref 之前將 isEditing=false 設定了嗎,那麼 v-if 不應該顯示按鈕了嗎?” 這就是虛擬 DOM 發揮作用的地方。因為 Vue 試圖最佳化和批次更改,所以它不會在我們設定 isEditing 為 false 時立即更新 DOM。因此,當我們呼叫 focusOnEditButton() 時,“編輯”按鈕尚未渲染。
相反,我們需要等到 Vue 完成下一個 DOM 更新週期之後。為此,Vue 元件有一個特殊的方法,叫做 $nextTick()。此方法接受一個回撥函式,然後在 DOM 更新後執行。
由於 focusOnEditButton() 方法需要在 DOM 更新後呼叫,我們可以將現有的函式體包裝在一個 $nextTick() 呼叫中。
focusOnEditButton() {
this.$nextTick(() => {
const editButtonRef = this.$refs.editButton;
editButtonRef.focus();
});
}
現在,當您透過鍵盤啟用“編輯”按鈕,然後取消或儲存更改時,焦點應該返回到“編輯”按鈕。成功!
Vue 生命週期方法
接下來,我們需要在點選“編輯”按鈕時將焦點移動到編輯表單的 <input> 元素。但是,由於我們的編輯表單與“編輯”按鈕在不同的元件中,因此我們不能只在“編輯”按鈕的點選事件處理程式中設定焦點。相反,我們可以利用每次點選“編輯”按鈕時都會移除並重新安裝 ToDoItemEditForm 元件這一事實來處理這個問題。
那麼,這如何運作呢?好吧,Vue 元件經歷一系列事件,被稱為 **生命週期**。此生命週期涵蓋了從元素被建立並新增到 VDOM(掛載)之前一直到它們從 VDOM 中刪除(銷燬)的所有過程。
Vue 允許您使用 **生命週期方法** 在生命週期的各個階段執行方法。這對於資料獲取等操作非常有用,您可能需要在元件渲染之前或某個屬性更改後獲取資料。生命週期方法的列表如下所示,按它們觸發的順序排列。
beforeCreate()— 在建立元件例項之前執行。資料和事件尚不可用。created()— 在元件初始化後,但元件新增到 VDOM 之前執行。這通常是資料獲取發生的地方。beforeMount()— 在編譯模板後,但在將元件渲染到實際 DOM 之前執行。mounted()— 在元件掛載到 DOM 後執行。可以在此處訪問refs。beforeUpdate()— 在元件中的資料更改時執行,但在更改渲染到 DOM 之前執行。updated()— 在元件中的資料更改後,並在更改渲染到 DOM 後執行。beforeDestroy()— 在元件從 DOM 中刪除之前執行。destroyed()— 在元件從 DOM 中刪除後執行。activated()— 僅在用特殊keep-alive標籤包裝的元件中使用。在元件啟用後執行。deactivated()— 僅在用特殊keep-alive標籤包裝的元件中使用。在元件停用後執行。
注意: Vue 文件提供了一個 很好的圖表,用於視覺化這些鉤子何時發生。來自 Digital Ocean 社群部落格的這篇文章更深入地介紹了生命週期方法。
既然我們已經瞭解了生命週期方法,讓我們使用其中一種方法在 ToDoItemEditForm 元件掛載時觸發焦點。
在 ToDoItemEditForm.vue 中,將 ref="labelInput" 附加到 <input> 元素,如下所示
<input
:id="id"
ref="labelInput"
type="text"
autocomplete="off"
v-model.lazy.trim="newName" />
接下來,在元件物件內新增一個 mounted() 屬性——注意,這應該放在 methods 屬性內,而應該與 props、data() 和 methods 處於相同的層次結構級別。 生命週期方法是特殊的獨立方法,而不是與使用者定義的方法並列。這應該不接受任何輸入。請注意,您不能在此處使用箭頭函式,因為我們需要訪問 this 來訪問我們的 labelInput ref。
mounted() {
}
在 mounted() 方法內,將您的 labelInput ref 分配給一個變數,然後呼叫 ref 的 focus() 函式。您無需在此處使用 $nextTick(),因為元件在呼叫 mounted() 時已新增到 DOM 中。
mounted() {
const labelInputRef = this.$refs.labelInput;
labelInputRef.focus();
}
現在,當您透過鍵盤啟用“編輯”按鈕時,焦點應該立即移動到編輯 <input>。
處理刪除待辦事項時的焦點
還有一個地方我們需要考慮焦點管理:當用戶刪除待辦事項時。在點選“編輯”按鈕時,將焦點移動到編輯名稱文字框,並在從編輯螢幕取消或儲存時返回到“編輯”按鈕是有意義的。
但是,與編輯表單不同,當元素被刪除時,我們沒有一個明確的焦點移動位置。我們還需要一種方法為輔助技術使用者提供確認元素已刪除的資訊。
我們已經在列表標題(App.vue 中的 <h2>)中跟蹤元素的數量,並且它與我們的待辦事項列表相關聯。這使得它成為刪除節點時移動焦點的合理位置。
首先,我們需要在列表標題中新增一個 ref。我們還需要向它新增一個 tabindex="-1"——這使得元素可程式設計地聚焦(即可以透過 JavaScript 聚焦),而預設情況下它不可聚焦。
在 App.vue 中,像這樣更新您的 <h2>
<h2 id="list-summary" ref="listSummary" tabindex="-1">{{listSummary}}</h2>
注意: tabindex 是一個非常強大的工具,可以處理某些無障礙問題。但是,應該謹慎使用。過度使用 tabindex="-1" 會給各種使用者帶來問題,所以只在需要的地方使用它。你幾乎不應該使用 tabindex > = 0,因為它會給使用者帶來問題,因為它會導致 DOM 流和 Tab 鍵順序不匹配,或者在 Tab 鍵順序中新增非互動式元素。這可能會讓使用者感到困惑,尤其是那些使用螢幕閱讀器和其他輔助技術的使用者。
現在我們有了 ref,並且讓瀏覽器知道我們可以在程式設計上將焦點放到 <h2> 上,我們需要將焦點放到它上面。在 deleteToDo() 的末尾,使用 listSummary ref 將焦點放到 <h2> 上。由於 <h2> 始終在應用程式中呈現,因此你不必擔心使用 $nextTick() 或生命週期方法來處理聚焦它。
deleteToDo(toDoId) {
const itemIndex = this.ToDoItems.findIndex((item) => item.id === toDoId);
this.ToDoItems.splice(itemIndex, 1);
this.$refs.listSummary.focus();
}
現在,當從列表中刪除專案時,焦點應該移動到列表標題。這應該為我們所有使用者提供合理的焦點體驗。
總結
這就是焦點管理和我們的應用程式!恭喜你完成了我們所有的 Vue 教程。在下一篇文章中,我們將介紹一些額外的資源,以幫助你進一步學習 Vue。
注意:如果你需要將程式碼與我們的版本進行比較,可以在我們的 todo-vue 儲存庫中找到示例 Vue 應用程式程式碼的完成版本。有關執行的即時版本,請參閱 https://mdn.github.io/todo-vue/。