Vue refs 和生命週期方法用於焦點管理

我們幾乎完成了 Vue 的學習。最後一個要關注的功能是焦點管理,換句話說,是如何改善我們應用程式的鍵盤可訪問性。我們將研究使用 **Vue refs** 來處理它——這是一個高階功能,它允許您直接訪問虛擬 DOM 下面的底層 DOM 節點,或者從一個元件直接訪問子元件的內部 DOM 結構。

先決條件

熟悉核心 HTMLCSSJavaScript 語言,瞭解 終端/命令列

Vue 元件是用管理應用程式資料的 JavaScript 物件和對映到底層 DOM 結構的基於 HTML 的模板語法組合而成的。為了安裝,並使用 Vue 的一些更高階功能(例如單檔案元件或渲染函式),您需要一個安裝了 node + npm 的終端。

目標 學習如何使用 Vue refs 處理焦點管理。

焦點管理問題

雖然我們已經有了可用的編輯功能,但我們並沒有為非滑鼠使用者提供良好的體驗。具體來說,當用戶啟用“編輯”按鈕時,我們會從 DOM 中刪除“編輯”按鈕,但我們不會將使用者的焦點移動到任何地方,因此它實際上消失了。這對鍵盤和非視覺使用者來說可能會令人困惑。

瞭解當前發生的情況

  1. 重新載入頁面,然後按 Tab 鍵。您應該在新增新待辦事項的輸入框上看到焦點輪廓。
  2. 再次按 Tab 鍵。焦點應該移動到“新增”按鈕。
  3. 再次點選它,它將出現在第一個複選框上。再按一次,焦點應該出現在第一個“編輯”按鈕上。
  4. 透過按 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。像這樣更新它

html
<button
  type="button"
  class="btn"
  ref="editButton"
  @click="toggleToItemEditForm">
  Edit
  <span class="visually-hidden">{{label}}</span>
</button>

為了訪問與我們的 ref 關聯的值,我們使用元件例項提供的 $refs 屬性。為了檢視當我們點選“編輯”按鈕時 ref 的值,在我們的 toggleToItemEditForm() 方法中新增一個 console.log(),如下所示

js
toggleToItemEditForm() {
  console.log(this.$refs.editButton);
  this.isEditing = true;
}

如果您此時啟用“編輯”按鈕,您應該在控制檯中看到一個 HTML <button> 元素的引用。

Vue 的 $nextTick() 方法

我們希望在使用者儲存或取消編輯時將焦點設定為“編輯”按鈕。為此,我們需要在 ToDoItem 元件的 itemEdited()editCancelled() 方法中處理焦點。

為了方便起見,建立一個不帶任何引數的新方法,名為 focusOnEditButton()。在其中,將您的 ref 分配給一個變數,然後呼叫 ref 上的 focus() 方法。

js
focusOnEditButton() {
  const editButtonRef = this.$refs.editButton;
  editButtonRef.focus();
}

接下來,在 itemEdited()editCancelled() 方法的末尾新增對 this.focusOnEditButton() 的呼叫

js
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 試圖最佳化和批次更改,所以它不會在我們設定 isEditingfalse 時立即更新 DOM。因此,當我們呼叫 focusOnEditButton() 時,“編輯”按鈕尚未渲染。

相反,我們需要等到 Vue 完成下一個 DOM 更新週期之後。為此,Vue 元件有一個特殊的方法,叫做 $nextTick()。此方法接受一個回撥函式,然後在 DOM 更新後執行。

由於 focusOnEditButton() 方法需要在 DOM 更新後呼叫,我們可以將現有的函式體包裝在一個 $nextTick() 呼叫中。

js
focusOnEditButton() {
  this.$nextTick(() => {
    const editButtonRef = this.$refs.editButton;
    editButtonRef.focus();
  });
}

現在,當您透過鍵盤啟用“編輯”按鈕,然後取消或儲存更改時,焦點應該返回到“編輯”按鈕。成功!

Vue 生命週期方法

接下來,我們需要在點選“編輯”按鈕時將焦點移動到編輯表單的 <input> 元素。但是,由於我們的編輯表單與“編輯”按鈕在不同的元件中,因此我們不能只在“編輯”按鈕的點選事件處理程式中設定焦點。相反,我們可以利用每次點選“編輯”按鈕時都會移除並重新安裝 ToDoItemEditForm 元件這一事實來處理這個問題。

那麼,這如何運作呢?好吧,Vue 元件經歷一系列事件,被稱為 **生命週期**。此生命週期涵蓋了從元素被建立並新增到 VDOM(掛載)之前一直到它們從 VDOM 中刪除(銷燬)的所有過程。

Vue 允許您使用 **生命週期方法** 在生命週期的各個階段執行方法。這對於資料獲取等操作非常有用,您可能需要在元件渲染之前或某個屬性更改後獲取資料。生命週期方法的列表如下所示,按它們觸發的順序排列。

  1. beforeCreate() — 在建立元件例項之前執行。資料和事件尚不可用。
  2. created() — 在元件初始化後,但元件新增到 VDOM 之前執行。這通常是資料獲取發生的地方。
  3. beforeMount() — 在編譯模板後,但在將元件渲染到實際 DOM 之前執行。
  4. mounted() — 在元件掛載到 DOM 後執行。可以在此處訪問 refs
  5. beforeUpdate() — 在元件中的資料更改時執行,但在更改渲染到 DOM 之前執行。
  6. updated() — 在元件中的資料更改後,並在更改渲染到 DOM 後執行。
  7. beforeDestroy() — 在元件從 DOM 中刪除之前執行。
  8. destroyed() — 在元件從 DOM 中刪除後執行。
  9. activated() — 僅在用特殊 keep-alive 標籤包裝的元件中使用。在元件啟用後執行。
  10. deactivated() — 僅在用特殊 keep-alive 標籤包裝的元件中使用。在元件停用後執行。

既然我們已經瞭解了生命週期方法,讓我們使用其中一種方法在 ToDoItemEditForm 元件掛載時觸發焦點。

ToDoItemEditForm.vue 中,將 ref="labelInput" 附加到 <input> 元素,如下所示

html
<input
  :id="id"
  ref="labelInput"
  type="text"
  autocomplete="off"
  v-model.lazy.trim="newName" />

接下來,在元件物件內新增一個 mounted() 屬性——注意,這應該放在 methods 屬性內,而應該與 propsdata()methods 處於相同的層次結構級別。 生命週期方法是特殊的獨立方法,而不是與使用者定義的方法並列。這應該不接受任何輸入。請注意,您不能在此處使用箭頭函式,因為我們需要訪問 this 來訪問我們的 labelInput ref。

js
mounted() {

}

mounted() 方法內,將您的 labelInput ref 分配給一個變數,然後呼叫 reffocus() 函式。您無需在此處使用 $nextTick(),因為元件在呼叫 mounted() 時已新增到 DOM 中。

js
mounted() {
   const labelInputRef = this.$refs.labelInput;
   labelInputRef.focus();
}

現在,當您透過鍵盤啟用“編輯”按鈕時,焦點應該立即移動到編輯 <input>

處理刪除待辦事項時的焦點

還有一個地方我們需要考慮焦點管理:當用戶刪除待辦事項時。在點選“編輯”按鈕時,將焦點移動到編輯名稱文字框,並在從編輯螢幕取消或儲存時返回到“編輯”按鈕是有意義的。

但是,與編輯表單不同,當元素被刪除時,我們沒有一個明確的焦點移動位置。我們還需要一種方法為輔助技術使用者提供確認元素已刪除的資訊。

我們已經在列表標題(App.vue 中的 <h2>)中跟蹤元素的數量,並且它與我們的待辦事項列表相關聯。這使得它成為刪除節點時移動焦點的合理位置。

首先,我們需要在列表標題中新增一個 ref。我們還需要向它新增一個 tabindex="-1"——這使得元素可程式設計地聚焦(即可以透過 JavaScript 聚焦),而預設情況下它不可聚焦。

App.vue 中,像這樣更新您的 <h2>

html
<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() 或生命週期方法來處理聚焦它。

js
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/