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