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/