From 9689f4871d4c698a3f53d1c2a57bacb2059ba2fc Mon Sep 17 00:00:00 2001 From: hanyujie2002 <84226578+hanyujie2002@users.noreply.github.com> Date: Sat, 6 Jan 2024 10:16:42 +0800 Subject: [PATCH 1/8] zh-cn: init translation of advanced svelte --- .../index.md | 886 ++++++++++++++++++ 1 file changed, 886 insertions(+) create mode 100644 files/zh-cn/learn/tools_and_testing/client-side_javascript_frameworks/svelte_reactivity_lifecycle_accessibility/index.md diff --git a/files/zh-cn/learn/tools_and_testing/client-side_javascript_frameworks/svelte_reactivity_lifecycle_accessibility/index.md b/files/zh-cn/learn/tools_and_testing/client-side_javascript_frameworks/svelte_reactivity_lifecycle_accessibility/index.md new file mode 100644 index 00000000000000..22f0d73d25ec6c --- /dev/null +++ b/files/zh-cn/learn/tools_and_testing/client-side_javascript_frameworks/svelte_reactivity_lifecycle_accessibility/index.md @@ -0,0 +1,886 @@ +--- +title: "Svelte 进阶:响应性、生命周期以及无障碍" +slug: Learn/Tools_and_testing/Client-side_JavaScript_frameworks/Svelte_reactivity_lifecycle_accessibility +page-type: learn-module-chapter +--- + +{{LearnSidebar}} +{{PreviousMenuNext("Learn/Tools_and_testing/Client-side_JavaScript_frameworks/Svelte_components","Learn/Tools_and_testing/Client-side_JavaScript_frameworks/Svelte_stores", "Learn/Tools_and_testing/Client-side_JavaScript_frameworks")}} + +在上一篇文章中,我们为待办事项列表添加了更多功能,并开始将应用程序组织成组件。在本文中,我们将添加应用程序的最终功能,并进一步组件化我们的应用程序。我们将学习处理与更新对象和数组相关的响应性问题。为了避免常见的问题,我们需要深入了解 Svelte 的响应性系统。我们还将解决一些无障碍聚焦问题,以及其他一些问题。 + + + + + + + + + + + + +
前提: +

+ 至少,建议你熟悉核心 HTML< + /a>、CSS JavaScript 语言,并且了解终端/命令行的使用。 +

+

+ 你需要安装了 Node.js 和 npm 的终端来编译和构建应用程序。 +

+
目标: + 学习一些高级的 Svelte 技巧,包括解决响应性问题、与组件生命周期相关的键盘无障碍问题等等。 +
+ +我们将重点关注涉及焦点管理的一些无障碍问题。为此,我们将利用一些访问 DOM 节点和执行 [`focus()`](/zh-CN/docs/Web/API/HTMLElement/focus) 和 [`select()`](/zh-CN/docs/Web/API/HTMLInputElement/select) 等方法的技术。我们还将学习如何声明以及清除 DOM 元素的事件监听器。 + +我们也需要学习一些组件生命周期的知识,以了解这些 DOM 节点何时从 DOM 中挂载(mount)和卸载(unmount),以及我们如何访问它们。我们还将学习 `action` 指令,它将允许我们以可重用和声明性的方式扩展 HTML 元素的功能。 + +最后,我们将进一步了解组件。到目前为止,我们已经看到组件如何使用属性共享数据,并使用事件和双向数据绑定与其父组件通信。现在,我们将看到组件如何公开方法和变量。 + +本文将在课程中开发以下新组件: + +- `MoreActions`:显示*全选*和*移除已完成*按钮,并发出处理其功能所需的相应事件。 +- `NewTodo`:显示用于添加新待办事项的 `` 字段和 *Add*(添加)按钮。 +- `TodosStatus`:显示“x out of y items completed”(“x 个已完成,共 y 个项目”)的状态标题。 + +## 与我们一起编码 + +### Git + +使用以下命令克隆 GitHub 仓库(如果尚未完成): + +```bash +git clone https://github.com/opensas/mdn-svelte-tutorial.git +``` + +然后运行以下命令以进入当前应用状态: + +```bash +cd mdn-svelte-tutorial/05-advanced-concepts +``` + +或者直接下载文件夹的内容: + +```bash +npx degit opensas/mdn-svelte-tutorial/05-advanced-concepts +``` + +记得运行 `npm install && npm run dev` 以在开发模式下启动应用。 + +### REPL + +要使用 REPL 与我们一起编码,请访问以下链接: + + + +## 处理 MoreActions 组件 + +现在我们来处理*全选*和*移除已完成*按钮。让我们创建一个组件,这个组件负责显示这些按钮并发出相应的事件。 + +1. 创建新文件,`components/MoreActions.svelte`。 +2. 当点击第一个按钮时,我们将发出 `checkAll` 事件,表示所有待办事项应该被全选或取消全选。当点击第二个按钮时,我们将发出 `removeCompleted` 事件,表示所有已完成的待办事项应该被移除。将以下内容放入你的 `MoreActions.svelte` 文件中: + + ```svelte + + +
+ + +
+ ``` + + 我们还包含了 `completed` 变量,用于在全选和取消全选之间进行切换。 + +3. 在 `Todos.svelte` 中,我们要导入我们的 `MoreActions` 组件,并创建两个函数来处理 `MoreActions` 组件发出的事件。 + + 在现有的导入语句下面添加以下导入语句: + + ```js + import MoreActions from "./MoreActions.svelte"; + ``` + +4. 然后在 ` + +
+ + +
+ ``` + + 我们还声明了响应式的 `completedTodos` 变量,用于启用或禁用*删除已完成*按钮。 + +2. 不要忘记从 `Todos.svelte` 中调用组件的位置将属性传递给 `MoreActions`: + + ```svelte + checkAllTodos(e.detail)} + on:removeCompleted={removeCompletedTodos} + /> + ``` + +## 使用 DOM:关注细节 + +现在我们已经完成了应用程序的所有必需功能,接下来我们将专注于一些无障碍功能,以改善我们的应用程序对仅使用键盘或屏幕阅读器的用户的可用性。 + +在当前状态下,我们的应用程序存在一些键盘无障碍问题,比如说在焦点管理方面。让我们了解一下这些问题。 + +## 探索我们的待办事项应用中的键盘无障碍问题 + +目前,对于使用键盘的用户来说,我们应用的焦点流动不够可预测或连贯。 + +如果你点击应用顶部的输入框,该输入框周围会出现粗实线框。这个实线框是你的视觉指示器,表示浏览器当前聚焦在这个元素上。 + +如果你是使用鼠标的用户,可能会忽略这个视觉提示。但是,如果你完全使用键盘工作,知道哪个控件有焦点非常重要。它告诉我们哪个控件将接收我们的按键输入。 + +如果你反复按下 Tab 键,你会看到实线焦点指示器在页面上的所有可以获得焦点的元素之间循环。如果将焦点移到*编辑*按钮上并按下 Enter 键,突然焦点就消失了,我们无法再知道哪个控件将接收我们的按键输入。 + +此外,如果按下 EscapeEnter 键,什么都不会发生。如果点击*取消*或*保存*,焦点再次消失。对于使用键盘的用户来说,这很令人困惑。 + +我们还希望添加一些实用特性,例如在必填字段为空时禁用*保存*按钮,在文本输入获得焦点时给予某些 HTML 元素焦点以及在文本输入获得焦点时自动选择内容。 + +要实现所有这些功能,我们需要以编程方式访问 DOM 节点,以运行诸如 [`focus()`](/zh-CN/docs/Web/API/HTMLElement/focus) 和 [`select()`](/zh-CN/docs/Web/API/HTMLInputElement/select) 等函数。我们还必须使用 [`addEventListener()`](/zh-CN/docs/Web/API/EventTarget/addEventListener) 和 [`removeEventListener()`](/zh-CN/docs/Web/API/EventTarget/removeEventListener) 以在控件获得焦点时运行特定的任务。 + +问题是,所有这些 DOM 节点是由 Svelte 在运行时动态创建的。因此,我们必须等待直到它们被创建并添加到 DOM 中,才能使用它们。为此,我们需要学习关于[组件生命周期](https://learn.svelte.dev/tutorial/onmount)的知识,以了解何时可以访问它们(稍后再详细讨论)。 + +## 创建 NewTodo 组件 + +让我们首先将我们的创建待办事项的表单提取到独立的组件中。根据我们目前所了解的知识,我们可以创建新的组件文件,并调整代码以发出 `addTodo` 事件,来传递新待办事项的名称和其他详细信息。 + +1. 创建名为 `components/NewTodo.svelte` 的新文件。 +2. 将以下内容放入其中: + + ```svelte + + +
e.key === 'Escape' && onCancel()}> +

+ +

+ + +
+ ``` + + 这里我们使用 `bind:value={name}` 将 `` 绑定到 `name` 变量,并使用 `disabled={!name}` 来在输入为空(即没有文本内容)时禁用*添加*按钮。我们还使用 `on:keydown={(e) => e.key === 'Escape' && onCancel()}` 处理了 Escape 键。当按下 Escape 键时,我们运行 `onCancel()`,这个函数会清空 `name` 变量。 + +3. 现在我们需要从 `Todos.svelte` 中导入并使用它,并更新 `addTodo()` 函数,使其接收新待办事项的名称作为实参。 + + 在 `Todos.svelte` 中的其他 `import` 语句下面添加以下 `import` 语句: + + ```js + import NewTodo from "./NewTodo.svelte"; + ``` + +4. 并将 `addTodo()` 函数更新为以下内容: + + ```js + function addTodo(name) { + todos = [...todos, { id: newTodoId, name, completed: false }]; + } + ``` + + `addTodo()` 现在直接接收新待办事项的名称,因此我们不再需要 `newTodoName` 变量来提供值。这部分由我们的 `NewTodo` 组件处理。 + + > **备注:** `{ name }` 的语法只是 `{ name: name }` 的简写形式。它来自 JavaScript 本身,与 Svelte 无关,只是为 Svelte 的缩写语法提供了一些灵感。 + +5. 最后,在此部分中,将 NewTodo 表单标记替换为对 `NewTodo` 组件的调用,如下所示: + + ```svelte + + addTodo(e.detail)} /> + ``` + +## 使用 `bind:this={dom_node}` 指令处理 DOM 节点 + +现在,我们希望在每次按下*添加*按钮后,`NewTodo` 组件的 `` 元素重新获得焦点。为此,我们需要对输入框的 DOM 节点的引用。Svelte 提供了一个方法来实现这一点,那就是使用 `bind:this={dom_node}` 指令。当指定了该指令后,一旦组件被挂载并且其 DOM 节点创建完成,Svelte 就会将对该 DOM 节点的引用分配给指定的变量。 + +我们将创建 `nameEl` 变量,并使用 `bind:this={nameEl}` 将其绑定到输入框上。然后在 `addTodo()` 中,在添加新待办事项后,我们将调用 `nameEl.focus()` 来重新将焦点设置到 `` 上。当用户按下 Escape 键时,在 `onCancel()` 函数中,我们也将执行相同的操作。 + +请将 `NewTodo.svelte` 的内容更新如下: + +```svelte + + +
e.key === 'Escape' && onCancel()}> +

+ +

+ + +
+``` + +尝试一下应用程序:在 `` 字段中输入新的待办事项名称,按下 Tab 以将焦点转移到*添加*按钮,然后按下 EnterEscape 键,可以看到输入框重新获得焦点。 + +### 为输入框设置自动聚焦 + +接下来,我们将为 `NewTodo` 组件添加 `autofocus` 属性,以指定 `` 字段在页面加载时获得焦点。 + +1. 我们首先尝试的方法如下:尝试添加 `autofocus` 属性,并在 ` + +

+ {completedTodos} out of {totalTodos} items completed +

+ ``` + +3. 在 `Todos.svelte` 的开头导入该文件,在其他导入语句下面添加以下 `import` 语句: + + ```js + import TodosStatus from "./TodosStatus.svelte"; + ``` + +4. 将 `Todos.svelte` 中的 `

` 状态标题替换为对 `TodosStatus` 组件的调用,将 `todos` 作为属性传递给它,如下所示: + + ```svelte + + ``` + +5. 进行一些清理工作,从 `Todos.svelte` 中移除 `totalTodos` 和 `completedTodos` 变量。只需移除 `$:totalTodos = …` 和 `$:completedTodos = …` 行,还要在计算 `newTodoId` 时不再使用 `totalTodos`,转而使用 `todos.length`。要做到这一点,请使用以下内容替换以 `let newTodoId` 开头的块: + + ```js + $: newTodoId = todos.length ? Math.max(...todos.map((t) => t.id)) + 1 : 1; + ``` + +6. 一切都按预期工作——我们刚刚将最后一部分标记提取到了独立的组件中。 + +现在,我们需要找到一种方法,在删除待办事项后将焦点设置在 `

` 状态标题上。 + +到目前为止,我们已经看到如何通过属性将信息传送到组件,并且组件可以通过发出事件或使用双向数据绑定与其父组件进行通信。子组件可以使用 `bind:this={dom_node}` 来获取对 `

` 节点的引用,并使用双向数据绑定将其暴露给外部。但是,这样做将破坏组件的封装性;将焦点设置在组件上应该是组件自己的责任。 + +因此,我们需要 `TodosStatus` 组件公开一个方法,供其父组件调用以将焦点放在 `

` 标题上。组件需要公开一些行为或信息以供使用者使用,这是一种非常常见的情况;让我们看看如何在 Svelte 中实现这一点。 + +我们已经了解 Svelte 使用 `export let varname = …` 来[声明属性](https://svelte.dev/docs/svelte-components#script-1-export-creates-a-component-prop)。但是,如果你不使用 `let` 导出 `const`、`class` 或 `function`,那么它们在组件外部是只读的。然而,函数表达式是有效的属性。在下面的示例中,前三个声明是属性,其余的是导出的值: + +```svelte + +``` + +有了这个理解,我们回到我们的用例。我们将创建一个名为 `focus()` 的函数,它将焦点放在 `

` 标题上。为此,我们需要 `headingEl` 变量来保存对 DOM 节点的引用,并使用 `bind:this={headingEl}` 将其绑定到 `

` 元素上。我们的聚焦方法只需运行 `headingEl.focus()`。 + +1. 更新 `TodosStatus.svelte` 的内容如下: + + ```svelte + + +

+ {completedTodos} out of {totalTodos} items completed +

+ ``` + + 注意,我们在 `

` 上添加了 `tabindex` 属性,以便允许元素通过编程方式接收焦点。 + + 正如我们之前所看到的,使用 `bind:this={headingEl}` 指令可以将 DOM 节点的引用存储在变量 `headingEl` 中。然后,我们使用 `export function focus()` 公开一个函数,该函数将焦点放在 `

` 标题上。 + + 我们如何从父组件访问这些导出的值呢?正如你可以使用 `bind:this={dom_node}` 指令绑定到 DOM 元素一样,你也可以使用 `bind:this={component}` 指令绑定到组件实例本身。因此,当你在 HTML 元素上使用 `bind:this` 时,你会得到对 DOM 节点的引用,当你在 Svelte 组件上使用它时,你会得到对该组件实例的引用。 + +2. 要绑定到 `TodosStatus` 实例,我们首先在 `Todos.svelte` 中创建 `todosStatus` 变量。在 `import` 语句下面添加以下行: + + ```js + let todosStatus; // 对 TodosStatus 实例的引用 + ``` + +3. 接下来,在调用中添加 `bind:this={todosStatus}` 指令,如下所示: + + ```svelte + + + ``` + +4. 现在,我们可以从 `removeTodo()` 函数中调用 `exported focus()` 方法: + + ```js + function removeTodo(todo) { + todos = todos.filter((t) => t.id !== todo.id); + todosStatus.focus(); // 将焦点放在状态标题上 + } + ``` + +5. 回到你的应用程序。现在,如果删除任何待办事项,状态标题将获得焦点。突出显示待办事项数量的变化,无论是对于视觉用户还是对于屏幕阅读器用户都很有用。 + +> **备注:** 你可能想知道为什么需要为组件绑定声明一个新变量。为什么不能直接调用 `TodosStatus.focus()`?这是因为应用可能同时有多个 `TodosStatus` 实例,因此需要引用特定实例的方法。这就是为什么需要指定变量来绑定特定实例。 + +## 到目前为止的代码 + +### Git + +若想要看到本文结束后程序代码所呈现的最终结果,请获取我们仓库的复制: + +```bash +cd mdn-svelte-tutorial/06-stores +``` + +或直接下载文件夹的内容: + +```bash +npx degit opensas/mdn-svelte-tutorial/06-stores +``` + +记得运行 `npm install && npm run dev` 以在开发模式下启动应用程序。 + +### REPL + +要在 REPL 中查看代码的当前状态,请访问: + + + +## 总结 + +在本文中,我们已经向应用程序添加所有所需的功能,同时我们还解决了一些无障碍和可用性问题。我们还将应用程序拆分为可管理的组件,每个组件都有独特的任务。 + +与此同时,我们学习了一些进阶的 Svelte 技术,包括: + +- 在更新对象和数组时处理响应性的注意事项 +- 使用 `bind:this={dom_node}`(绑定 DOM 元素)来处理 DOM 节点 +- 使用组件生命周期函数 `onMount()` +- 使用 `tick()` 函数强制 Svelte 解决待处理的状态更改 +- 使用 `use:action` 指令以可重用且声明性的方式为 HTML 元素添加功能 +- 使用 `bind:this={component}`(绑定组件)访问组件方法 + +在下一篇文章中,我们将学习如何使用 store 在组件之间进行通信,并为我们的组件添加动画效果。 + +{{PreviousMenuNext("Learn/Tools_and_testing/Client-side_JavaScript_frameworks/Svelte_components","Learn/Tools_and_testing/Client-side_JavaScript_frameworks/Svelte_stores", "Learn/Tools_and_testing/Client-side_JavaScript_frameworks")}} From 4d5815c83c62eeb843e87f7e2dda8b3e37ba23e7 Mon Sep 17 00:00:00 2001 From: hanyujie2002 Date: Sat, 6 Jan 2024 10:26:53 +0800 Subject: [PATCH 2/8] Apply suggestions from code review Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .../index.md | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/files/zh-cn/learn/tools_and_testing/client-side_javascript_frameworks/svelte_reactivity_lifecycle_accessibility/index.md b/files/zh-cn/learn/tools_and_testing/client-side_javascript_frameworks/svelte_reactivity_lifecycle_accessibility/index.md index 22f0d73d25ec6c..fd164dc6d3e0bb 100644 --- a/files/zh-cn/learn/tools_and_testing/client-side_javascript_frameworks/svelte_reactivity_lifecycle_accessibility/index.md +++ b/files/zh-cn/learn/tools_and_testing/client-side_javascript_frameworks/svelte_reactivity_lifecycle_accessibility/index.md @@ -1,7 +1,6 @@ --- -title: "Svelte 进阶:响应性、生命周期以及无障碍" +title: Svelte 进阶:响应性、生命周期以及无障碍 slug: Learn/Tools_and_testing/Client-side_JavaScript_frameworks/Svelte_reactivity_lifecycle_accessibility -page-type: learn-module-chapter --- {{LearnSidebar}} @@ -16,7 +15,7 @@ page-type: learn-module-chapter

至少,建议你熟悉核心 HTML< - /a>、CSS JavaScript 语言,并且了解` 字段和 *Add*(添加)按钮。 +- `NewTodo`:显示用于添加新待办事项的 `` 字段和 _Add_(添加)按钮。 - `TodosStatus`:显示“x out of y items completed”(“x 个已完成,共 y 个项目”)的状态标题。 ## 与我们一起编码 @@ -160,7 +159,7 @@ npx degit opensas/mdn-svelte-tutorial/05-advanced-concepts 为了弄清楚发生这种情况的原因,我们需要了解在更新数组和对象时 Svelte 中的响应性是如何工作的。 -许多 Web 框架使用虚拟 DOM 技术来更新页面。基本上,虚拟 DOM 是网页内容的内存副本。框架更新这个虚拟表示,然后将其与“真实” DOM 同步。这比直接更新 DOM 要快得多,并允许框架应用许多优化技术。 +许多 Web 框架使用虚拟 DOM 技术来更新页面。基本上,虚拟 DOM 是网页内容的内存副本。框架更新这个虚拟表示,然后将其与“真实”DOM 同步。这比直接更新 DOM 要快得多,并允许框架应用许多优化技术。 这些框架通常会在每次对虚拟 DOM 更改后重新运行我们的 JavaScript 代码,并应用不同的方法来缓存资源消耗巨大的计算和优化执行。它们几乎不会尝试理解我们的 JavaScript 代码在做什么。 @@ -502,7 +501,7 @@ const checkAllTodos = (completed) => { 那么这里发生了什么?当你在 Svelte 中更新组件的状态时,它不会立即更新 DOM。相反,它会等到下一个微任务来检查是否有其他需要应用的更改,包括其他组件。这样做可以避免不必要的工作,并允许浏览器更有效地批处理操作。 -在这种情况下,当 `editing` 是 `false` 时,编辑 `` 不可见,因为它不存在于 DOM中。在 `onEdit()` 函数中,我们将 `editing` 设置为 `true`,然后立即尝试访问 `nameEl` 变量并执行 `nameEl.focus()`。问题在于,Svelte 还没有更新 DOM。 +在这种情况下,当 `editing` 是 `false` 时,编辑 `` 不可见,因为它不存在于 DOM 中。在 `onEdit()` 函数中,我们将 `editing` 设置为 `true`,然后立即尝试访问 `nameEl` 变量并执行 `nameEl.focus()`。问题在于,Svelte 还没有更新 DOM。 解决这个问题的一种方法是使用 [`setTimeout()`](/zh-CN/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout) 函数,延迟调用 `nameEl.focus()`,直到下一个事件循环,并给 Svelte 更新 DOM 的机会。 @@ -668,7 +667,7 @@ node.addEventListener("focus", (event) => node.select()); ``` -3. 现在我们的 `onEdit()` 函数可以简单得多: +3. 现在我们的 `onEdit()` 函数可以简单得多: ```js function onEdit() { @@ -689,7 +688,7 @@ node.addEventListener("focus", (event) => node.select()); let editButtonPressed = false; // 跟踪编辑按钮是否已按下,以便在取消或保存后将焦点放在它上面 ``` -3. 接下来,我们将修改*编辑*按钮的特性来保存这个标志,并创建一个 action。像这样更新 `onEdit()` 函数: +3. 接下来,我们将修改*编辑*按钮的特性来保存这个标志,并创建一个 action。像这样更新 `onEdit()` 函数: ```js function onEdit() { @@ -704,7 +703,7 @@ node.addEventListener("focus", (event) => node.select()); const focusEditButton = (node) => editButtonPressed && node.focus(); ``` -5. 最后,我们在*编辑*按钮上使用 `focusEditButton` action,像这样: +5. 最后,我们在*编辑*按钮上使用 `focusEditButton` action,像这样: ```svelte