使用 Vue 模板引用进行焦点管理 - 学习 Web 开发

使用 Vue 模板引用进行焦点管理
=================

* 上一页
* Overview: Client-side JavaScript frameworks
* 下一页

使用 Vue 所做的事情快要结束了。最后一个需要讲述的内容是焦点管理，换句话说，也是如何提升我们应用的键盘无障碍性。我们将看看如何使用 **Vue 模板引用**来处理这个问题——这是一个进阶功能，允许你直接访问虚拟 DOM 之下的底层 DOM 节点，或者从一个组件直接访问一个子组件的内部 DOM 结构。

|  |  |
| --- | --- |
| 前提： | 熟悉核心的 HTML、CSS 和 JavaScript 语言，了解终端/命令行知识。

 Vue 组件是由管理应用程序数据的 JavaScript 对象和映射到基础 DOM
 结构的基于 HTML 的模板语法组成的。为了进行安装并使用 Vue
 的一些更高级的功能（例如“单文件组件（SFC）”或渲染功能），你将需要一个装有
 node + npm 的终端。
  |
| 目标： | 学习如何使用 Vue 模板引用（ref）进行焦点管理。 |

焦点管理问题
------

虽然我们完成了有效的编辑功能，但我们并没有为非鼠标用户提供良好的体验。具体来说，当用户激活“Edit”按钮时，我们将“Edit”按钮从 DOM 中移除，但我们并没有将用户的焦点移动到任何地方，所以实际上它只是消失了。这对键盘用户和非视觉用户来说可能会产生迷惑性。

了解一下发生了什么：

1. 重新加载页面，然后按下 `Tab` 键。你可以看到在添加新的待办事项的输入框中看到一个表示聚焦状态的框线。
2. 再次按下 `Tab` 键。焦点应当移动到“Add”按钮。
3. 再次按下 `Tab` 键。这次它应该在第一个复选框上。再按一次，焦点会在第一个“Edit”按钮上。
4. 按下 `Enter` 键激活“Edit”按钮。复选框将替换为我们的编辑组件，但聚焦线就消失了。

这种行为会很不协调。此外，当再次按下 `Tab` 时会发生什么，取决于你所使用的浏览器。同样地，如果你保存或取消了你的编辑，当你移回非编辑视图时，焦点会再次消失。

为了给用户一个更好的体验，我们将添加代码来控制焦点，以便在编辑表单显示时将其设置为编辑字段。当用户取消或保存他们的编辑时，我们也要把焦点放回“Edit”按钮上。为了设置焦点，我们需要多了解一点 Vue 的内部工作方式。

虚拟 DOM 和模板引用
------------

Vue 和其他一些框架一样，使用一个虚拟 DOM（VDOM）来管理元素。这意味着 Vue 在内存中保留了我们应用程序中所有节点的表示。任何更新首先在内存中的节点上执行，然后所有需要对页面上的实际节点进行的更改都会被批量同步。

由于读写实际的 DOM 节点通常比虚拟节点开销更大，虚拟 DOM 可以带来更好的性能。然而，这也意味着在使用框架时，你往往不应该直接通过原生浏览器 API（如 `Document.getElementById`）来编辑你的 HTML 元素，因为这会导致 VDOM 和真实 DOM 不同步。

相反，如果你需要访问底层 DOM 节点（比如设置焦点时），你可以使用 Vue 模板引用。对于自定义的 Vue 组件，你也可以使用 refs 来直接访问子组件的内部结构，然而这样做应该是谨慎的，因为这会使代码更难推理和理解。

要在一个组件中使用模板引用，你需要在你想访问的元素上添加一个 `ref` 属性，并为该属性的值添加一个字符串标识符。值得注意的是，在一个组件中，引用必须是唯一的。在同一时间呈现的两个元素不应该有相同的引用。

### 向我们的应用添加模板引用

现在，我们向 `ToDoItem.vue` 中的“Edit”按钮附加模板引用。像这样更新代码：

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

为了访问与我们的引用相关的值，我们使用组件实例上提供的 `$refs` 属性。当我们点击“Edit”按钮时，要看到 ref 的值，在我们的 `toggleToItemEditForm()` 方法中添加 `console.log()`，像这样：

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

如果你在这时激活“Edit”按钮，你应该看到控制台中引用了一个 HTML `<button>` 元素。

Vue 的 $nextTick() 方法
--------------------

我们想在用户保存或取消他们的编辑时将焦点设置在“Edit”按钮上。要做到这一点，我们需要在 `ToDoItem` 组件的 `itemEdited()` 和 `editCancelled()` 方法中处理焦点。

为了方便起见，创建一个不需要参数的新方法，叫做 `focusOnEditButton()`。在这个方法中，把你的 `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 是 undefined 的*”。这看起来很奇怪。当你激活“Edit”按钮时，你的按钮的 ref 之前是被定义的，但现在它不是了。这到底是怎么回事？

记住，当我们把 `isEditing` 改为 `true` 时，我们不再渲染“Edit”按钮的组件部分。这意味着没有元素可以绑定引用，所以它变成了 `undefined`。

你现在可能在想：“我们在访问 `ref` 之前不是设置了 `isEditing=false` 吗，现在 `v-if` 不是应该显示这个按钮吗？”这就是虚拟 DOM 起作用的地方。因为 Vue 试图优化和批处理变化，当我们把 `isEditing` 设置为 `false` 时，它不会立即更新 DOM。所以当我们调用 `focusOnEditButton()` 时，“Edit”按钮还没有被渲染。

相反，我们需要等到 Vue 经历了下一个 DOM 更新周期之后。为了做到这一点，Vue 组件有一个特殊的方法，叫做 `$nextTick()`。这个方法接受一个回调函数，然后在 DOM 更新后执行。

由于 `focusOnEditButton()` 方法需要在 DOM 更新后被调用，我们可以将现有的函数体包裹在 `$nextTick()` 调用中。

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

现在，当你激活“Edit”按钮，然后通过键盘取消或保存你的修改时，焦点应该返回到“Edit”按钮。成功了！

Vue 生命周期方法
----------

接下来，我们需要在点击“Edit”按钮时将焦点移到编辑表单的 `<input>` 元素。然而，由于我们的编辑表单和“Edit”按钮在不同的组件中，我们不能仅仅在“Edit”按钮的点击事件处理器中设置焦点。相反，我们可以利用每当“Edit”按钮被点击时，我们就删除并重新挂载 `ToDoItemEditForm` 组件的这一事实来处理这个问题。

那么，这是如何工作的呢？Vue 组件经历了一系列的事件，被称为**生命周期**。这个生命周期从元素被创建并添加到 VDOM（*mounted*）之前，一直到它们被从 VDOM 中移除（*destroyed*）。

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` 标签包装的组件。在组件被停止激活后运行。

**备注：** Vue 文档提供了一个很好的图表，可以直观地看到这些钩子何时发生。这篇文章来自 Digital Ocean 社区博客，更深入地探讨了生命周期方法。

现在我们已经了解了生命周期方法，让我们用一个方法在我们的 `ToDoItemEditForm` 组件被安装时触发焦点。

在 `ToDoItemEditForm.vue` 中，像这样向 `<input>` 元素附加 `ref="labelInput"` 属性：

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

接下来，在你的组件对象中添加一个 `mounted()` 属性——**注意，这不应该放在 `methods` 属性里面，而应该与 `props`、`data()` 和 `methods` 处于同一个层次**。生命周期方法是独立的特殊方法，不应与用户定义方法放在一起。这些方法不接受参数。注意，你不能在这里使用箭头函数，因为我们需要访问 `this` 来访问我们的 `labelInput` 模板引用。

```js
mounted() {

}
```

在 `mounted()` 方法中，将你的 `labelInput` 引用分配给一个变量，然后调用该引用的 `focus()` 函数。你不必在这里使用 `$nextTick`，因为当 `mounted()` 被调用时，该组件已经被添加到 DOM 中。

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

现在，当你用键盘激活“Edit”按钮时，焦点应该立即被移到待编辑的 `<input>` 元素上。

删除待办事项项目时的焦点管理
--------------

还有一个地方我们需要考虑焦点管理：当用户删除一个待办事项时。当点击“Edit”按钮时，将焦点移到编辑名称文本框中是有意义的，而当取消或从编辑界面保存时，将焦点移回“Edit”按钮。

然而，与编辑表格不同的是，当一个元素被删除时，我们没有一个明确的位置让焦点转移。我们还需要一种方法来为辅助技术用户提供信息，确认一个元素被删除。

我们已经在追踪我们的列表标题中的元素数量——`App.vue` 中的 `<h2>`——并且它与我们的待办事项列表相关。这使得当我们删除一个节点时，它是一个合理的移动焦点的地方。

首先，我们需要给我们的列表标题添加一个引用。我们还需要给它添加一个 `tabindex="-1"`，这使得该元素可以通过程序聚焦（即可以通过 JavaScript 聚焦），而默认情况下它是不可以的。

在 `App.vue` 中，更新你的 `<h2>`，如下所示：

```html
<h2 id="list-summary" ref="listSummary" tabindex="-1">{{listSummary}}</h2>
```

**备注：** `tabindex` 是一个真正强大的工具，可以处理某些无障碍问题。然而，应该谨慎地使用它。过度使用 `tabindex="-1"` 会给各种用户带来问题，所以只在需要的地方使用它。你也几乎不应该使用 `tabindex` > = `0`，因为它可能给用户带来问题，因为它可能使 DOM 流和标签顺序不匹配，和/或在标签顺序中添加非交互式元素。这可能会让用户感到困惑，特别是那些使用屏幕阅读器和其他辅助技术的用户。

现在我们有了一个 `ref`，并且让浏览器知道我们可以通过编程聚焦到 `<h2>` 上，我们需要对它设置焦点。在 `deleteToDo()` 的结尾，使用 `listSummary` 引用来设置焦点在 `<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 教程。在下一篇文章中，我们将用一些进一步的资源来结束你的 Vue 学习。

**备注：** 如果你需要对照我们的版本检查你的代码，你可以在我们的 todo-vue 仓库中找到一个完成版本的 Vue 应用示例代码。关于运行中的实时版本，请看 https://mdn.github.io/todo-vue/。

* 上一页
* Overview: Client-side JavaScript frameworks
* 下一页


使用 Vue event、method 和 model 添加一个新的 todo 表单 - 学习 Web 开发

使用 Vue event、method 和 model 添加一个新的 todo 表单
==========================================

* 上一页
* Overview: Client-side JavaScript frameworks
* 下一页

我们现在已经有了样本数据，还有一个循环，它获取每一位数据并将其呈现在我们应用程序的 `ToDoItem` 中。接下来我们真正需要的是允许我们的用户在应用程序中输入他们自己的待办事项的能力，为此我们需要一个“text”类型的 `<input>`，当数据输入时触发的事件提交，一个在提交时触发以添加数据并重新呈现列表的方法，以及一个控制数据的模型。这就是我们将在本文中介绍的内容。

|  |  |
| --- | --- |
| 前提： |
 熟悉核心 HTML、CSS
 和 JavaScript 语言，了解
 终端/命令行。

 Vue 组件被编写为管理应用程序数据的 JavaScript 对象和映射到底层 DOM
 结构的基于 HTML 的模板语法的组合。对于安装，以及使用 Vue
 的一些更高级的功能（如单文件组件或渲染功能），你需要一个安装了 node +
 npm 的终端。
  |
| 目标： | 了解如何在 Vue 中处理表单，以及通过关联、事件、模型和方法。 |

创建一个新的待办事项表单
------------

我们的 app 可以展示待办事项列表，但是我们不能更新该列表，除非手动更改代码。让我们新建一个组件来允许我们添加新的待办项。

1. 在 components 目录下，新建文件 `ToDoForm.vue`。
2. 创建一个空的 `<template>` 和 `<script>`：

```html
<template></template>

<script>
 export default {};
</script>
```

3. 新建一个 HTML 表单来允许我们输入新的待办项并把它提交到 app。我们需要一个 `<form>`，它里面包含一个 `<label>`、一个 `<input>`、一个 `<button>`。更新后的模版如下：

```html
<template>
  <form>
    <label for="new-todo-input"> What needs to be done? </label>
    <input
 type="text"
 id="new-todo-input"
 name="new-todo"
 autocomplete="off" />
    <button type="submit">Add</button>
  </form>
</template>
```

因此，现在我们有一个 form 组件可以用来输入新的待办项的标题，它最终会渲染成 `ToDoItem` 的 label。
4. 我们把这个组件添加到 app 中，返回 `App.vue` 然后在 `<script>` 添加下面的语句：

```js
import ToDoForm from "./components/ToDoForm";
```

5. 在你的 App 组件中注册它

```js
components: {
  ToDoItem, ToDoForm;
}
```

6. 最后将 `ToDoForm` 组件添加到 App 中的 `<template>` 中，像下面这样：

```html
<template>
  <div id="app">
    <h1>My To-Do List</h1>
    <to-do-form></to-do-form>
    <ul>
      <li v-for="item in ToDoItems" :key="item.id">
        <to-do-item
 :label="item.label"
 :done="item.done"
 :id="item.id"></to-do-item>
      </li>
    </ul>
  </div>
</template>
```

现在，当你查看正在运行的站点时，你应该会看到显示的新表单。

![Our todo list app rendered with a text input to enter new todos](/zh-CN/docs/Learn/Tools_and_testing/Client-side_JavaScript_frameworks/Vue_methods_events_models/rendered-form-with-text-input.png)

如果你填写并单击“添加”按钮，页面会将表单发送回服务器，但这并不是我们真正想要的。我们真正想要做的是在 `submit` 事件上运行一个方法，该方法将添加 `App` 中定义的 `ToDoItem` 数据列表的新待办事项。为此，我们需要向组件实例添加一个方法。

创建一个方法并用 v-on 绑定这个方法到一个事件上
--------------------------

我们需要将方法添加到 `ToDoForm` 组件对象中，以使方法可用。这是在组件的 `methods` 属性中完成的，它与 `data()`、`props` 等位于同一位置。`methods` 属性包含我们可能需要在组件中调用的任何方法。引用时，方法已完全运行，因此使用它们在模板内显示信息不是一个好主意。为了显示通过计算获得的数据，你应该使用 `computed` 属性，我们稍后会介绍。

1. 在这个组件中，我们需要在 `ToDoForm` 组件对象内的 `methods` 属性中添加一个 `onSubmit()` 方法。我们将使用它来处理提交操作。就像这样：

```js
export default {
  methods: {
    onSubmit() {
      console.log("form submitted");
    },
  },
};
```

2. 接下来我们需要将该方法绑定到我们的 `<form>` 元素的 `submit` 事件处理程序。就像 Vue 如何使用 `v-bind` 语法来绑定属性，Vue 有用于事件处理的特殊指令：`v-on`。`v-on` 指令通过 `v-on:event="method"` 语法工作。和 `v-bind` 很像，还有一个简写语法：`@event="method"`。为了保持一致性，我们将在此处使用简写语法。将 `submit` 处理程序添加到你的 `<form>` 元素，如下所示：

```html
<form @submit="onSubmit"></form>
```

3. 当你运行此程序时，应用程序仍会将数据发布到服务器，从而导致刷新。由于我们在客户端上进行所有处理，因此没有服务器来处理回发。我们还会在页面刷新时丢失所有本地状态。为了防止浏览器发布到服务器，我们需要阻止事件的默认操作通过页面冒泡（`Event .preventDefault()`，在原生 JavaScript 中）。Vue 有一个特殊的语法叫做 **event modifiers** 可以在我们的模板中为我们处理这个问题。修饰符被附加到事件的末尾，带有一个点，如下所示：`@event.modifier`。以下是事件修饰符列表：
    * `.stop`：停止传播事件。等效于常规 JavaScript 事件中的 `Event.stopPropagation()`。
    * `.prevent`：阻止事件的默认行为。等效于 `Event.preventDefault()`。
    * `.self`：仅当事件是从该确切元素分派时触发处理程序。
    * `{.key}`：仅通过指定键触发事件处理程序。 MDN 有一个有效键值列表 (en-US); 多词键只需转换为 kebab 大小写（例如 `page-down`）。
    * `.native`：监听组件根（最外层的包装）元素上的原生事件。
    * `.once`：监听事件，直到它被触发一次，然后不再触发。
    * `.left`：仅通过鼠标左键事件触发处理程序。
    * `.right`：仅通过鼠标右键事件触发处理程序。
    * `.middle`：仅通过鼠标中键事件触发处理程序。
    * `.passive`：等效于在 vanilla JavaScript 中使用 `addEventListener()` 创建事件监听器时传入 `{ passive: true }` 参数。在这种情况下，我们需要使用 `.prevent` 处理程序来停止浏览器的默认提交操作。将 `.prevent` 添加到模板中的 `@submit` 处理程序中，如下所示：

```js
<form @submit.prevent="onSubmit">
```

如果你现在尝试提交表单，你会注意到页面没有重新加载。如果打开控制台，可以看到我们在 `onSubmit()` 方法中添加的 `console.log()` 所输出结果。

用 v-model 来绑定数据到输入
------------------

接下来，我们需要一种从表单的 `<input>` 中获取值的方法，这样我们就可以将新的待办事项添加到我们的 `ToDoItems` 数据列表中。

我们首先需要的是表单中的 `data` 属性来跟踪待办事项的值。

1. 向我们的 `ToDoForm` 组件对象添加一个 `data()` 方法，该方法返回一个 `label` 字段。我们可以将 `label` 的初始值设置为空字符串。
 你的组件对象现在应该如下所示：

```js
export default {
  methods: {
    onSubmit() {
      console.log("form submitted");
    },
  },
  data() {
    return {
      label: "",
    };
  },
};
```

2. 我们现在需要一些方法将 `new-todo-input` 元素字段的值附加到 `label` 字段。Vue 对此有一个特殊的指令：`v-model`。`v-model` 绑定到你在其上设置的数据属性，并使其与 `<input>` 保持同步。`v-model` 适用于所有不同的输入类型，包括复选框、单选框和选择输入。要使用 `v-model`，你需要向 `<input>` 添加一个结构为 `v-model="variable"` 的属性。
 所以在我们的例子中，我们会将它添加到我们的 `new-todo-input` 字段中，如下所示。现在就这样做：

```js
<input
  type="text"
  id="new-todo-input"
  name="new-todo"
  autocomplete="off"
  v-model="label"
/>
```

**备注：** 你还可以通过事件和 `v-bind` 属性的组合将数据与 `<input>` 值同步。事实上，这就是 `v-model` 在幕后所做的。但是，确切的事件和属性组合因输入类型而异，并且比仅使用 `v-model` 快捷方式需要更多代码。
3. 让我们通过记录在我们的 `onSubmit()` 方法中提交的数据的值来测试我们对 `v-model` 的使用。在组件中，使用 `this` 关键字访问数据属性。所以我们使用 `this.label` 访问我们的 `label` 字段。
 更新你的 `onSubmit()` 方法，使其如下所示：

```js
methods: {
  onSubmit() {
    console.log('Label value: ', this.label);
  }
},
```

4. 现在回到你正在运行的应用程序，在 `<input>` 字段中添加一些文本，然后单击“添加”按钮。你应该会看到你输入的值已记录到控制台，例如：
```

Label value: My value
```

使用修饰符改变 `v-model` 的行为
---------------------

与事件修饰符类似，我们也可以添加修饰符来改变 `v-model` 的行为。在我们的案例中，有两个值得考虑。第一个，`.trim`，将删除输入之前或之后的空格。我们可以将修饰符添加到我们的 `v-model` 语句中，如下所示：`v-model.trim="label"`。

我们应该考虑的第二个修饰符称为 `.lazy`。当 `v-model` 同步文本输入的值时，此修饰符会发生变化。如前所述，`v-model` 同步通过使用事件更新变量来工作。对于文本输入，此同步使用 `input` 事件进行。通常，这意味着 Vue 在每次击键后都会同步数据。`.lazy` 修饰符导致 `v-model` 使用 `change` 事件代替。这意味着 Vue 只会在输入失去焦点或提交表单时同步数据。就我们的目的而言，这更合理，因为我们只需要最终数据。

要同时使用 `.lazy` 修饰符和 `.trim` 修饰符，我们可以将它们链接起来，例如 `v-model.lazy.trim="label"`。

如上所示更新你的 `v-model` 属性以链接 `lazy` 和 `trim`，然后再次测试你的应用。例如，尝试提交一个两端都有空格的值。

使用自定义事件将数据传递给父级
---------------

我们现在非常接近能够将新的待办事项添加到我们的列表中。接下来我们需要做的是将新创建的待办事项传递给我们的 `App` 组件。为此，我们可以让我们的 `ToDoForm` 发出一个传递数据的自定义事件，并让 `App` 监听它。这与 HTML 元素上的原生事件非常相似：子组件可以发出可以通过 `v-on` 监听的事件。

在 `ToDoForm` 的 `onSubmit` 事件中，我们添加一个 `todo-added` 事件。自定义事件的发射方式如下：`this.$emit("event-name")`。重要的是要知道事件处理程序区分大小写并且不能包含空格。Vue 模板也被转换为小写，这意味着 Vue 模板无法监听以大写字母命名的事件。

1. 将 `onSubmit()` 方法中的 `console.log()` 替换为以下内容：

```js
this.$emit("todo-added");
```

2. 接下来，回到 `App.vue` 并添加一个 `methods` 属性到包含 `addToDo()` 方法的组件对象，如图所示 以下。目前，此方法只需将 `To-do added` 记录到控制台即可。

```js
export default {
  name: "app",
  components: {
    ToDoItem,
    ToDoForm,
  },
  data() {
    return {
      ToDoItems: [
        { id: uniqueId("todo-"), label: "Learn Vue", done: false },
        {
          id: uniqueId("todo-"),
          label: "Create a Vue project with the CLI",
          done: true,
        },
        { id: uniqueId("todo-"), label: "Have fun", done: true },
        { id: uniqueId("todo-"), label: "Create a to-do list", done: false },
      ],
    };
  },
  methods: {
    addToDo() {
      console.log("To-do added");
    },
  },
};
```

3. 接下来，将 `todo-added` 事件的事件监听器添加到 `<to-do-form></to-do-form>`，它在事件触发时调用 `addToDo()` 方法。使用 `@` 简写，监听器看起来像这样：`@todo-added="addToDo"`:

```html
<to-do-form @todo-added="addToDo"></to-do-form>
```

4. 当你提交 `ToDoForm` 时，你应该会看到来自 `addToDo()` 方法的控制台日志。这很好，但我们仍然没有将任何数据传递回 `App.vue` 组件。我们可以通过将额外的参数传递给 `ToDoForm` 组件中的 `this.$emit()` 函数来做到这一点。
 在这种情况下，当我们触发事件时，我们希望将 `label` 数据连同它一起传递。这是通过在 `$emit()` 方法中包含你要作为另一个参数传递的数据来完成的：`this.$emit("todo-added", this.label)`。这类似于原生 JavaScript 事件如何包含数据，除了自定义 Vue 事件默认不包含事件对象。这意味着发出的事件将直接匹配你提交的任何对象。所以在我们的例子中，我们的事件对象只是一个字符串。
 像这样更新你的 `onSubmit()` 方法：

```js
onSubmit() {
  this.$emit('todo-added', this.label)
}
```

5. 要真正在 `App.vue` 中获取这些数据，我们需要向我们的 `addToDo()` 方法添加一个参数，其中包含 `label` 新的待办事项。返回 `App.vue` 并立即更新：

```js
methods: {
  addToDo(toDoLabel) {
    console.log('To-do added:', toDoLabel);
  }
}
```

如果你再次测试你的表单，你将看到在提交时输入的任何文本都已记录到控制台中。Vue 自动将 `this.$emit()` 中事件名称后面的参数传递给你的事件处理程序。

将新的待办事项添加到我们的数据中
----------------

现在我们在 `App.vue` 中获得了来自 `ToDoForm` 的数据，我们需要在 `ToDoItems` 数组中添加一个表示它的项目。这可以通过将新的待办事项对象推送到包含我们新数据的数组来完成。

1. 像这样更新你的 `addToDo()` 方法：

```js
addToDo(toDoLabel) {
  this.ToDoItems.push({id:uniqueId('todo-'), label: toDoLabel, done: false});
}
```

2. 再次尝试测试你的表单，你应该会看到新的待办事项被附加到列表的末尾。
3. 在我们继续之前，让我们做进一步的改进。如果你在输入为空时提交表单，则没有文本的待办事项仍会添加到列表中。为了解决这个问题，我们可以防止在 name 为空时触发 todo-added 事件。由于 `.trim` 指令已经对 name 进行了修剪，因此我们只需要测试空字符串。回到你的 `ToDoForm` 组件，像这样更新 `onSubmit()` 方法。如果标签值为空，我们就不发出 `todo-added` 事件。

```js
onSubmit() {
  if(this.label === "") {
    return;
  }
  this.$emit('todo-added', this.label);
}
```

4. 再次尝试你的表单。现在你将无法将空项目添加到待办事项列表中。

![我们的待办事项列表应用程序使用文本输入呈现以输入新待办事项](/zh-CN/docs/Learn/Tools_and_testing/Client-side_JavaScript_frameworks/Vue_methods_events_models/rendered-form-with-new-items.png)

使用 `v-model` 更新输入值
------------------

在我们的 `ToDoForm` 组件中还有一件事需要修复——提交后，`<input>` 仍然包含旧值。但这很容易解决——因为我们使用 `v-model` 将数据绑定到 `ToDoForm` 中的 `<input>`，如果 我们将 name 参数设置为空字符串，输入也会更新。

将你的 `ToDoForm` 组件的 `onSubmit()` 方法更新为：

```js
onSubmit() {
  if(this.label === "") {
    return;
  }
  this.$emit('todo-added', this.label);
  this.label = "";
}
```

现在，当你单击“添加”按钮时，“new-todo-input”将自行清除。

总结
--

非常好。我们现在可以将待办事项添加到我们的表单中！我们的应用程序现在开始感觉具有交互性，但一个问题是到目前为止我们完全忽略了它的外观和感觉。在下一篇文章中，我们将集中精力解决这个问题，看看 Vue 为组件样式提供的不同方式。

* 上一页
* Overview: Client-side JavaScript frameworks
* 下一页


组件化我们的 React App - 学习 Web 开发

组件化我们的 React App
================

* 上一页
* Overview: Client-side JavaScript frameworks
* 下一页  (en-US)

此时，我们的 App 是一个单体。在我们能使用它之前，我们需要把它分解成可管理的、可描述的组件。React 对于什么是组件和什么不是组件并没有任何硬性规定，这完全取决于你！在这篇文章，我们将向你展示一种合理分解 App 的方法。

|  |  |
| --- | --- |
| 前提： |
 熟悉核心 HTML、CSS
 和 JavaScript 语言，了解终端/命令行。
  |
| 目标： | 展示一种合理分解待办事项 App 的方法。 |

定义第一个组件
-------

在缺乏经验的情况下，定义一个组件会比较棘手，不过这些提示可以帮助你：

* 如果它在程序里看起来是个明显的“块”，那它可能是个组件。
* 如果它在程序里经常会被复用，那它可能是个组件。

第二条很有价值：使用通用的 UI 元素作为组件，可以让你只更改一处，就能更改所有使用该组件的地方。现在你也不必马上把所有东西都拆成组件。让我们从第二点开始做起，将 UI 中重用最多、最重要的部分抽取为一个组件：一个待办事件项。

编写 `<Todo />`
-------------

在制作组件之前，我们应该为它新建一个文件。实际上我们应该为它新建一个文件夹。下面的命令可以新建一个 `component` 文件夹，在这之中又新建了一个叫做 `Todo.js` 的文件。在你运行这些命令之前，请确保你在 App 的根目录下。

```bash
mkdir src/components
touch src/components/Todo.js
```

新的 `Todo.js` 文件是空的。打开它，并添加第一行。

```js
import React from "react";
```

既然我们要做一个叫做 `Todo` 的组件，就要在 `Todo.js` 中加入相关代码。如下所示，这段代码中，我们使用了一行代码定义了函数并导出了它。

```js
export default function Todo() {
  return (
    // …
  );
}
```

目前为止很顺利，但我们的组件必须返回一些东西！回到 `src/App.js`，从无序列表中复制第一个 `<li>` 并将其粘贴到 `Todo.js`，代码看起来像这样：

```js
export default function Todo() {
  return (
    <li className="todo stack-small">
      <div className="c-cb">
        <input id="todo-0" type="checkbox" defaultChecked={true} />
        <label className="todo-label" htmlFor="todo-0">
          Eat
        </label>
      </div>
      <div className="btn-group">
        <button type="button" className="btn">
          Edit <span className="visually-hidden">Eat</span>
        </button>
        <button type="button" className="btn btn\_\_danger">
          Delete <span className="visually-hidden">Eat</span>
        </button>
      </div>
    </li>
  );
}
```

**备注：** 组件必须返回一些东西。如果你试图渲染一个不返回任何东西的组件，React 会在浏览器中提示出现错误。

到现在为止，我们的 `Todo` 组件已经完成，我们可以使用它了。在 `App.js` 中，在文件顶部添加以下代码，导入 `Todo` 组件：

```js
import Todo from "./components/Todo";
```

导入组件之后，你可以调用 `<Todo />` 组件来替换 `App.js` 中的所有 `<li>`。你的 `<ul>` 看起来应该像这样：

```js
<ul
  role="list"
  className="todo-list stack-large stack-exception"
  aria-labelledby="list-heading">
  <Todo />
  <Todo />
  <Todo />
</ul>
```

当你查看浏览器时，你会注意到一些不幸的事：现在你的清单重复显示了三次！

![在 App 中，Todo 组件重复出现，因为标签被硬编码到组件中](/zh-CN/docs/Learn/Tools_and_testing/Client-side_JavaScript_frameworks/React_components/todo-list-repeating-todos.png)

我们想得不仅仅是 Eat，我们还有其他事情要做。接下来我们将尝试使用不一样的方式来调用组件，渲染出不同的内容。

制作 *不同* 的 `<Todo />`
--------------------

组件之所以强大，是因为它们让我们可以重用 UI 的各个部分，并让我们可以引用一处作为 UI 各个部分的源头。问题是，我们通常不希望重复使用每个组件的全部内容；我们希望重复使用大多数部分，并改变小部分内容。这就是 props 的用处。

### `name` 里有什么？

为了跟踪我们要完成的任务名称，我们应该确保每个 `<Todo />` 组件渲染一个不同的名称。

在 `App.js` 中，给每个 `<Todo />` 传递一个 name prop。我们使用之前的任务名称来传递：

```js
<Todo name="Eat" />
<Todo name="Sleep" />
<Todo name="Repeat" />
```

当你刷新浏览器，你会看到和以前完全一样的东西。我们给了 `<Todo />` 一些 props，但还没有使用它们。让我们回到 `Todo.js` 修改一下。

首先修改你的 `Todo()` 函数定义，使其接受 `props` 参数。如果你想检查 `props` 是否被组件正确接收，你可以像之前那样使用 `console.log()` 检查你的 `props`。

一旦你确信你的组件得到了它的 `props`，你就可以用 `name` prop 来替换每次出现的 `Eat`。记住：当你在 JSX 表达式的中，你可以使用大括号来注入一个变量的值。

把所有这些操作汇总在一起，你的 `Todo()` 函数看起来应该像这样：

```js
export default function Todo(props) {
  return (
    <li className="todo stack-small">
      <div className="c-cb">
        <input id="todo-0" type="checkbox" defaultChecked={true} />
        <label className="todo-label" htmlFor="todo-0">
          {props.name}
        </label>
      </div>
      <div className="btn-group">
        <button type="button" className="btn">
          Edit <span className="visually-hidden">{props.name}</span>
        </button>
        <button type="button" className="btn btn\_\_danger">
          Delete <span className="visually-hidden">{props.name}</span>
        </button>
      </div>
    </li>
  );
}
```

*现在*你的浏览器应该显示三个不同的任务。但另一个问题仍然存在：它们都是被默认勾选的。

![现在有了不同标签的待办事项，标签以 props 的形式传入组件。](/zh-CN/docs/Learn/Tools_and_testing/Client-side_JavaScript_frameworks/React_components/todo-list-unique-todos.png)

### 是否 `completed`？

在原来的静态列表中，只有 `Eat` 被选中。我们想重新利用构成 `<Todo />` 组件的大部分 UI，但只修改一处。这个工作很适合另一个 prop! 在 `App.js` 中给每个 `<Todo />` 传递一个新的 props `completed`。第一个（`Eat`）的值应该是 `true`；剩下的应该是 `false`。

```js
<Todo name="Eat" completed={true} />
<Todo name="Sleep" completed={false} />
<Todo name="Repeat" completed={false} />
```

在这之前，我们必须确保 `Todo.js` 已经使用了这些 props。修改 `<input />` 上的 `defaultChecked` 属性，使其等于 `completed` prop。

```js
<input id="todo-0" type="checkbox" defaultChecked={props.completed} />
```

你的浏览器应该会更新，现在只有 `Eat` 被选中了。

![我们的待办事项现在有了不同的选中状态——一些复选框被选中了，其他的没有](/zh-CN/docs/Learn/Tools_and_testing/Client-side_JavaScript_frameworks/React_components/todo-list-differing-checked-states.png)

如果你修改每个 `<Todo />` 组件的 `completed` prop，你的浏览器会根据选中或不选中去相应的渲染复选框。

### 请给我一些 `id`

我们的 `<Todo />` 组件给每个任务的 `id` 属性都设为 `todo-0` 了。这是个不好的 HTML 写法，因为 `id` 属性必须是不同的（它们被 CSS、JavaScript 等用作文档片段的唯一标识符）。这意味着我们需要给每一个 `Todo` 组件一个不同 `id` prop。

为了遵循最初的规则，让我们给每个 `<Todo />` 组件实例一个 `todo-i` 格式的 ID，其中数字 `i` 每次增加一。

```js
<Todo name="Eat" completed={true} id="todo-0" />
<Todo name="Sleep" completed={false} id="todo-1" />
<Todo name="Repeat" completed={false} id="todo-2" />
```

现在回到 `Todo.js` 来使用 `id` prop。需要替换 `<input />` 元素的 `id` 属性，以及 label 的 `htmlFor` 属性。

```js
<div className="c-cb">
  <input id={props.id} type="checkbox" defaultChecked={props.completed} />
  <label className="todo-label" htmlFor={props.id}>
    {props.name}
  </label>
</div>
```

到目前为止，还算不错？
-----------

目前为止，我们使用 React 很顺利，不过可以做的更好！我们的代码有些重复。渲染我们的 `<Todo />` 组件的三行文字几乎是相同的，只有一点不同：每个 prop 的值。

我们使用一个 Javascript 的核心能力来整理我们的代码：迭代。想使用迭代，我们首先应该重新思考我们的任务。

任务作为数据
------

现在每个任务都包含三份信息：它的名称、是否被选中，及它唯一的 ID。这些数据可以很好地转化为一个对象。因为我们有多个任务，所以用一个对象数组可以很好地表示这些数据。

在 `src/index.js` 中，在最后一条导入下面，在 `ReactDOM.render()` 上面，新建一个 `const` 数据。

```js
const DATA = [
  { id: "todo-0", name: "Eat", completed: true },
  { id: "todo-1", name: "Sleep", completed: false },
  { id: "todo-2", name: "Repeat", completed: false },
];
```

然后，我们将 `DATA` 作为一个 prop 传入 `<App />`，叫做 `tasks`。`src/index.js` 的最后一行看起来应该像这样：

```js
ReactDOM.render(<App tasks={DATA} />, document.getElementById("root"));
```

这个数组现在可以在 App 组件中以 `props.tasks` 使用。你可以使用 `console.log()` 去检查它。

**备注：** `ALL_CAPS` 这个常量名称在 JavaScript 中没有特殊意义；它们是一种惯例，告诉其他开发者“这个数据在这里定义后将永远不会改变”。

使用迭代进行渲染
--------

为了渲染我们的对象数组，我们会把其中的每个对象变成一个 `<Todo />` 组件。JavaScript 给了我们一个数组方法，用于将数据转换为其他东西：`Array.prototype.map()`。

在 return 语句的上方，新建一个叫 `taskList` 的 `const`，然后使用 `map()` 去转换它。我们先把 `tasks` 数组变成简单的东西：每个任务的 `name`：

```js
const taskList = props.tasks?.map((task) => task.name);
```

我们尝试使用 `taskList` 替换所有 `<ul>` 的 children：

```js
<ul
  role="list"
  className="todo-list stack-large stack-exception"
  aria-labelledby="list-heading">
  {taskList}
</ul>
```

这让我们能够再次显示所有的组件，但我们还有更多的工作要做：目前浏览器将每个任务的名称显示为非结构化文本。我们忘记了 HTML 结构——`<li>`、复选框和按钮！

![我们的待办事项中，其标签刚好显示在一行上。](/zh-CN/docs/Learn/Tools_and_testing/Client-side_JavaScript_frameworks/React_components/todo-list-unstructured-names.png)

为了解决这个问题，我们需要从 `map()` 函数 return 一个 `<Todo />` 组件——记住 JSX 允许我们将 JavaScript 和标记结构混合起来！让我们用下面的方法替代已有的试试：

```js
const taskList = props.tasks.map((task) => <Todo />);
```

回顾一下你的 App；现在你的任务看起来更像以前的样子，不过它们缺少自身的名字。记住，我们映射的每个任务都有 `id`、`name` 和 `checked` 属性，我们要把这些属性传递给 `<Todo />` 组件。如果你把它们放在一起，我们就会得到这样的代码：

```js
const taskList = props.tasks.map((task) => (
  <Todo id={task.id} name={task.name} completed={task.completed} />
));
```

现在 App 看起来像之前一样了，而我们的代码也变得没有那么重复了。

特殊的 key
-------

现在 React 正在将我们的任务从一个数组中渲染出来，它必须跟踪谁是谁，以便正确的渲染它们。React 通过自行猜测来保持对每一项的追踪，不过我们通过给 `<Todo />` 传递一个 `key` prop 来帮助 React 保持追踪。`key` 是一个被 React 管理的特殊的 prop，你不能将其用于其他目的。

因为每个 key 必须是不同的，我们复用每个任务对象的 `id` 属性来作为它的 key。像这样更新你的 `taskList` 常量：

```js
const taskList = props.tasks.map((task) => (
  <Todo
    id={task.id}
    name={task.name}
    completed={task.completed}
    key={task.id}
  />
));
```

**你应该传递不同的 key 给任何使用迭代方式渲染的东西**。你的浏览器中不会有什么明显变化，不过如果你不使用不同的 key，React 会在你的 console 中记录一个警告，并且你的 App 可能会表现的很奇怪！

整合 App 的其他部分
------------

现在我们已经把最重要的组件整理好了，我们可以把我们 App 的其他部分也变成组件。记住，组件要么是明显的 UI 片段，要么是重复使用的 UI 片段，或者两者都是，我们可以再制作这两个组件：

* `<Form/>`
* `<FilterButton/>`

既然我们知道需要两个文件，我们可以使用一个终端命令来批量新建文件。在你的终端中运行这段代码，注意要在你的 App 的根目录下：

```bash
touch src/components/Form.js src/components/FilterButton.js
```

### `<Form />` 组件

打开 `components/Form.js` 并按照以下几点来做：

* 在顶部导入 `React`，像我们在 `Todo.js` 中做的一样。
* 做一个新的 `Form()` 组件，其基本结构与 `Todo()` 相同，并导出该组件。
* 从 `App.js` 中复制 `<form>` 标签和它们之间的所有内容，并将它们粘贴到 `Form()` 的 `return` 语句中。
* 在文件末尾导出 `Form`。

你的 `Form.js` 文件应该看起来像这样：

```js
import React from "react";

function Form(props) {
  return (
    <form>
      <h2 className="label-wrapper">
        <label htmlFor="new-todo-input" className="label\_\_lg">
          What needs to be done?
        </label>
      </h2>
      <input
        type="text"
        id="new-todo-input"
        className="input input\_\_lg"
        name="text"
        autoComplete="off"
      />
      <button type="submit" className="btn btn\_\_primary btn\_\_lg">
        Add
      </button>
    </form>
  );
}

export default Form;
```

### <FilterButton /> 组件

Do the same things you did to create `Form.js` inside `FilterButton.js`, but call the component `FilterButton()` and copy the HTML for the first button inside the `<div>` element with the `class` of `filters` from `App.js` into the `return` statement.

按照新建 `Form.js` 时的方法新建 `FilterButton.js`，但是组件名应该为 `FilterButton()` ，还要拷贝 `App.js` 中，`class` 为 `filters` 的 `<div>` 元素中的，第一个按钮的 HTML，然后放在 return 语句中。

文件看起来应该像这样：

```js
import React from "react";

function FilterButton(props) {
  return (
    <button type="button" className="btn toggle-btn" aria-pressed="true">
      <span className="visually-hidden">Show </span>
      <span>all </span>
      <span className="visually-hidden"> tasks</span>
    </button>
  );
}

export default FilterButton;
```

**备注：** 你可能会注意到，我们在这里犯了和第一次在 `<Todo />` 组件中同样的错误，即每个按钮将是相同的。没关系！我们将在以后修复这个组件，具体在 Back to the filter buttons (en-US)。

导入所有组件
------

让我们来使用所有的新组件。

在 `App.js` 的顶部再添加一些 `import` 语句，以导入它们。

然后更新 `App()` 中的 `return` 语句，使其渲染我们的组件。当你完成时，`App.js` 文件应该像这样：

```js
import React from "react";
import Form from "./components/Form";
import FilterButton from "./components/FilterButton";
import Todo from "./components/Todo";

function App(props) {
  const taskList = props.tasks.map((task) => (
    <Todo
      id={task.id}
      name={task.name}
      completed={task.completed}
      key={task.id}
    />
  ));
  return (
    <div className="todoapp stack-large">
      <h1>TodoMatic</h1>
      <Form />
      <div className="filters btn-group stack-exception">
        <FilterButton />
        <FilterButton />
        <FilterButton />
      </div>
      <h2 id="list-heading">3 tasks remaining</h2>
      <ul
        role="list"
        className="todo-list stack-large stack-exception"
        aria-labelledby="list-heading">
        {taskList}
      </ul>
    </div>
  );
}

export default App;
```

有了这些，我们*几乎*已经准备好在 React App 中处理一些交互性问题了！

总结
--

本文到此为止——我们已经深入探讨了如何将你的 App 很好地分解成组件，并有效地渲染它们。现在我们将继续研究如何在 React 中处理事件，并添加一些交互性。

* 上一页
* Overview: Client-side JavaScript frameworks
* 下一页  (en-US)


开始编写我们的 Svelte 待办事项列表应用程序 - 学习 Web 开发

开始编写我们的 Svelte 待办事项列表应用程序
=========================

* 上一页
* Overview: Client-side JavaScript frameworks
* 下一页

现在我们对 Svelte 运作机制有初步的了解后，就能开始构建我们的示例应用程序：一个待办事项列表。此篇文章中，会先确认应用程序所需的功能，接着会建立 `Todos.svelte` 组件并编写一些静态标记（markup）语言和样式，待一切准备就绪后，就能开始开发待办事项列表应用程序的相关特性，我们将在后续文章实现它们。

我们想要让用户能够浏览、新增和删除任务，也能标记它们为完成状态。这将是我们在本系列教程中要开发的基本功能，此外，在开发过程中我们将会学到一些更进阶的概念。

|  |  |
| --- | --- |
| 前提： |
 推荐你至少需熟悉基本的
 HTML、CSS 与
 JavaScript
 等程序语言且具备终端/命令行环境基本知识。

你需要安装了 node 和 npm 的终端来编译和构建你的应用程序。 |
| 目标： | 学习如何建立一个 Svelte 组件并于另一个组件内渲染它，通过属性（prop）传递数据并保存它的状态。 |

与我们一起编写代码
---------

### Git

使用以下命令克隆 Github 仓库（假如本地还没有代码）：

```bash
git clone https://github.com/opensas/mdn-svelte-tutorial.git
```

接着取得当前应用程序状态，执行：

```bash
cd mdn-svelte-tutorial/02-starting-our-todo-app
```

或像这样直接下载文件夹内容：

```bash
npx degit opensas/mdn-svelte-tutorial/02-starting-our-todo-app
```

记得执行 `npm install && npm run dev` 以开发模式来运行你的应用程序。

### REPL

为了使用 REPL 和我们一起编写程序代码，点击如下链接：

https://svelte.dev/repl/b7b831ea3a354d3789cefbc31e2ca495?version=3.23.2

待办列表应用程序特性
----------

这是我们预计完成待办列表应用程序的样子：

![典型的待办事项列表应用程序，标题为“what needs to be done”（需要完成的事项），输入框用于输入更多待办事项，待办事项列表带有复选框](/zh-CN/docs/Learn/Tools_and_testing/Client-side_JavaScript_frameworks/Svelte_Todo_list_beginning/01-todo-list-app.png)

使用这个用户界面，我们的用户将能够做到以下事情：

* 浏览他们的任务。
* 标记任务为已完成或待办理，而不是删除它们。
* 删除任务。
* 新增任务。
* 通过状态过滤任务：全部、正在进行或已完成任务。
* 编辑任务。
* 标记全部任务为正在进行或已完成状态。
* 删除全部已完成任务。
建立我们第一个组件
---------

建立 `Todos.svelte` 组件——这个组件包含我们的待办任务列表。

1. 建立新文件夹——`src/components`。

**备注：** 你可以把组件放在 `src` 文件夹当中的任何地方，但放在 `components` 文件夹是比较常见的做法，也让你可以更容易地找到它们。
2. 建立 `src/components/Todos.svelte` 文件并包含以下内容：

```html
<h1>Svelte To-Do list</h1>
```

3. 改变 `public/index.html` 中的 `title` 元素内容为 *Svelte To-do list*：

```html
<title>Svelte To-Do list</title>
```

4. 打开 `src/App.svelte` 并替换为以下内容：

```html
<script>
 import Todos from "./components/Todos.svelte";
</script>

<Todos />
```

5. 在开发模式中，如果定义属性没有存在于组件中，Svelte 将会在浏览器控制台发出警告；在此例中，当我们于 `src/main.js` 实例化 `App` 组件时，由于我们已经明确定义出 `name` 属性，但并没有实际在 `App` 中使用到，所以控制台现在应该会给你一个警告信息，如“<App> was created with unknown prop 'name'”。为了排除这个问题，请从 `src/main.js` 中移除 `name` 属性；`main.js` 现在应该类似于这样：

```js
import App from "./App.svelte";

const app = new App({
  target: document.body,
});

export default app;
```

假如你现在检查你的测试服务器 URL，应该会看到 `Todos.svelte` 组件已经渲染成如下画面：

![基本组件渲染，标题为“Svelte to-do list”](/zh-CN/docs/Learn/Tools_and_testing/Client-side_JavaScript_frameworks/Svelte_Todo_list_beginning/02-todos-component-rendered.png)

加上静态标记语言
--------

我们将会从应用程序的静态标记语言表示开始，所以你将会看到它的外观。复制并粘贴下面内容至 `Todos.svelte` 组件文件以取代现有内容：

```svelte
<!-- Todos.svelte -->
<div class="todoapp stack-large">
  <!-- 新待办 -->
  <form>
    <h2 class="label-wrapper">
      <label for="todo-0" class="label\_\_lg"> What needs to be done? </label>
    </h2>
    <input type="text" id="todo-0" autocomplete="off" class="input input\_\_lg" />
    <button type="submit" disabled="" class="btn btn\_\_primary btn\_\_lg">
      Add
    </button>
  </form>

  <!-- 过滤器 -->
  <div class="filters btn-group stack-exception">
    <button class="btn toggle-btn" aria-pressed="true">
      <span class="visually-hidden">Show</span>
      <span>All</span>
      <span class="visually-hidden">tasks</span>
    </button>
    <button class="btn toggle-btn" aria-pressed="false">
      <span class="visually-hidden">Show</span>
      <span>Active</span>
      <span class="visually-hidden">tasks</span>
    </button>
    <button class="btn toggle-btn" aria-pressed="false">
      <span class="visually-hidden">Show</span>
      <span>Completed</span>
      <span class="visually-hidden">tasks</span>
    </button>
  </div>

  <!-- 待办状态 -->
  <h2 id="list-heading">2 out of 3 items completed</h2>

  <!-- 待办 -->
  <ul role="list" class="todo-list stack-large" aria-labelledby="list-heading">
    <!-- 待办—1（编辑模式）-->
    <li class="todo">
      <div class="stack-small">
        <form class="stack-small">
          <div class="form-group">
            <label for="todo-1" class="todo-label">
              New name for 'Create a Svelte starter app'
            </label>
            <input
 type="text"
 id="todo-1"
 autocomplete="off"
 class="todo-text" />
          </div>
          <div class="btn-group">
            <button class="btn todo-cancel" type="button">
              Cancel
              <span class="visually-hidden">renaming Create a Svelte starter app</span>
            </button>
            <button class="btn btn\_\_primary todo-edit" type="submit">
              Save
              <span class="visually-hidden">new name for Create a Svelte starter app</span>
            </button>
          </div>
        </form>
      </div>
    </li>

    <!-- 待办—2 -->
    <li class="todo">
      <div class="stack-small">
        <div class="c-cb">
          <input type="checkbox" id="todo-2" checked />
          <label for="todo-2" class="todo-label">
            Create your first component
          </label>
        </div>
        <div class="btn-group">
          <button type="button" class="btn">
            Edit
            <span class="visually-hidden">Create your first component</span>
          </button>
          <button type="button" class="btn btn\_\_danger">
            Delete
            <span class="visually-hidden">Create your first component</span>
          </button>
        </div>
      </div>
    </li>

    <!-- 待办—3 -->
    <li class="todo">
      <div class="stack-small">
        <div class="c-cb">
          <input type="checkbox" id="todo-3" />
          <label for="todo-3" class="todo-label">
            Complete the rest of the tutorial
          </label>
        </div>
        <div class="btn-group">
          <button type="button" class="btn">
            Edit
            <span class="visually-hidden">Complete the rest of the tutorial</span>
          </button>
          <button type="button" class="btn btn\_\_danger">
            Delete
            <span class="visually-hidden">Complete the rest of the tutorial</span>
          </button>
        </div>
      </div>
    </li>
  </ul>

  <hr />

  <!-- 更多操作 -->
  <div class="btn-group">
    <button type="button" class="btn btn\_\_primary">Check all</button>
    <button type="button" class="btn btn\_\_primary">Remove completed</button>
  </div>
</div>
```

再次确认已经渲染出来的画面，你应该会看到如下样子：

![一个待办事项列表应用程序，但没有样式，标题为“what needs to be done”，有输入框、复选框等。](/zh-CN/docs/Learn/Tools_and_testing/Client-side_JavaScript_frameworks/Svelte_Todo_list_beginning/03-unstyled-todo-app.png)

上面 HTML 标记语言的样式并不是那么好看，而且也没有实际功能。尽管如此，让我们来看看这些标记，看看它们如何与我们想要的特性关联起来：

* 一个标签和一个输入框用来输入新任务。
* 三个按钮用来依据状态来过滤任务。
* 一个标签用来显示任务总数及已完成任务数量。
* 一个无序列表，其中包含每一个任务项目。
* 编辑任务时，列表条目会有输入框和两个按钮用来取消或保存变更。
* 任务处于非编辑状态时，有一个复选框用于设置已完成状态，还有两个按钮用于编辑或删除任务。
* 最后有两个按钮，分别用于选中/取消选中所有任务和删除已完成任务。

随后的文章我们会逐一将这些特性完善，以及实现更多其他特性。

### 待办列表的无障碍特性

你可能有注意到一些特殊的属性。例如：

```svelte
<button class="btn toggle-btn" aria-pressed="true">
  <span class="visually-hidden">Show</span>
  <span>All</span>
  <span class="visually-hidden">tasks</span>
</button>
```

在这里，`aria-pressed` 告诉无障碍技术（如屏幕阅读器）按钮可以是两种状态之一：`pressed` 或 `unpressed`。可以想象类似于开关状态。设定数值为 `true` 时，意谓着按钮默认是被按下的。

`visually-hidden` 类别现在还没有效果，因为我们还没引入任何 CSS。不过，一旦我们设置了样式，任何带有该类的元素都将对视力正常的用户隐藏起来，但屏幕阅读器用户仍然可以使用，这是因为视力正常的用户并不需要这些文字；这些文字是为了向屏幕阅读器用户提供更多有关按钮作用的信息，因为屏幕阅读器用户没有额外的视觉上下文来帮助他们。

再往下看一点，你可以找到如下 `<ul>` 元素：

```svelte
<ul
 role="list"
 className="todo-list stack-large"
 aria-labelledby="list-heading"></ul>
```

`role` 属性则帮助无障碍技术去解释元素具有什么种类的语义数值——或它的目的。默认情况下，`<ul>` 会被视为列表，但在加上样式之后会破坏原有性质。明确定义规则为“list”将能复原 `<ul>` 元素本身意义。如果你想要想进一步了解为什么有必要这样做，可以去参考 Scott O'Hara 的“修复列表”（2019）文章。

`aria-labelledby` 属性则告诉辅助技术，我们把具有 `id` 属性值为 `list-heading` 的 `<h2>` 元素作为描述下面列表用途的标签。这种关联为列表提供了更多的上下文信息，可以帮助屏幕阅读器用户更好地理解列表的目的。

这似乎是讨论 Svelte 如何处理无障碍的好时机；让我们继续看下去。

Svelte 无障碍性支持
-------------

Svelte 特别重视无障碍性。目的是鼓励开发人员尽可能“默认”编写更具有无障碍性的代码。作为编译程序，Svelte 能静态地分析我们的 HTML 模板，而当组件被编译后，就能提出一些无障碍警告。

无障碍性（缩写为 a11y）并不总是容易做到，但 Svelte 会在编写不具无障碍性的标记时发出警告，从而帮助你。

举例来说，假如我们新增 `<img>` 元素至我们的 `todos.svelte` 组件，但没有对应的 `alt` 属性：

```html
<h1>Svelte To-Do list</h1>

<img height="32" width="88" src="https://www.w3.org/WAI/wcag2A" />
```

编译器将会报出下列警告：

```bash
(!) Plugin svelte: A11y: <img> element should have an alt attribute
src/components/Todos.svelte
1: <h1>Svelte To-Do list</h1>
2:
3: <img height="32" width="88" src="https://www.w3.org/WAI/wcag2A">
   ^
created public/build/bundle.js in 220ms

[2020-07-15 04:07:43] waiting for changes...
```

此外，在我们调用编译器之前，编辑器也会显示这个警告：

![显示图像标签的代码编辑器窗口，并弹出错误消息，指出该元素应具有 alt 属性](/zh-CN/docs/Learn/Tools_and_testing/Client-side_JavaScript_frameworks/Svelte_Todo_list_beginning/04-svelte-accessibility-support.png)

你可以像这样使用以 `svelte-ignore` 开头的注释，来告诉 Svelte 去忽略下一个标记语言区块的警告：

```svelte
<!-- svelte-ignore a11y-missing-attribute -->
<img height="32" width="88" src="https://www.w3.org/WAI/wcag2A" />
```

**备注：** 通过 VSCode 你可以点击 *Quick fix...* 链接或按 `Ctrl` + `.` 来自动地加上这个忽略注释。

假如你想要全局地关闭这个警告，可以像这样在你的 `rollup.config.js` 文件中调整 `Svelte` 插件配置并补上这个 `onwarn` 处理器：

```js
plugins: [
  svelte({
    dev: !production,
    css: (css) => {
      css.write("public/build/bundle.css");
    },
    // 警告通常直接传递给 Rollup。你可以
    // 选择在这里处理它们，例如压制
    // 带有特定代码的警告
    onwarn: (warning, handler) => {
      // 例如，我不在乎屏幕阅读器 -> 请 不 要 这 样 做！！！
      if (warning.code === "a11y-missing-attribute") return;

      // 让 Rollup 正常处理所有其他警告
      handler(warning);
    },
  }),

  // …
];
```

在设计上，这些警告是在编译器本身中实现的，而不是可以选择性加入项目的插件。这样做的目的是在默认情况下检查标记中的无障碍问题，并让你选择不使用特定的警告。

**备注：** 只有在有充分理由的情况下，例如在构建快速原型时，才应禁用这些警告。做一名优秀的网络公民，让尽可能多的用户都能访问你的网页，这一点非常重要。

Svelte 通过 eslint-plugin-jsx-a11y 来检查无障碍性规则，它是提供在 JSX 元素方面的许多无障碍性规则静态检查的一个 ESLint 插件。Svelte 的目标是在其编译器中实现所有这些功能，其中大部分功能已经移植到 Svelte 中。你可以在 GitHub 中看到尚未实现的无障碍检查。点击规则的链接可以查阅它们各自的含义。

为标记语言加上样式
---------

让我们来将待办列表变得好看些。将 `public/global.css` 文件替换为如下内容：

```css
/\* 重置样式 \*/
\*,
\*::before,
\*::after {
  box-sizing: border-box;
}
\*:focus {
  outline: 3px dashed #228bec;
  outline-offset: 0;
}
html {
  font: 62.5% / 1.15 sans-serif;
}
h1,
h2 {
  margin-bottom: 0;
}
ul {
  list-style: none;
  padding: 0;
}
button {
  border: none;
  margin: 0;
  padding: 0;
  width: auto;
  overflow: visible;
  background: transparent;
  color: inherit;
  font: inherit;
  line-height: normal;
  -webkit-font-smoothing: inherit;
  -moz-osx-font-smoothing: inherit;
  -webkit-appearance: none;
}
button::-moz-focus-inner {
  border: 0;
}
button,
input,
optgroup,
select,
textarea {
  font-family: inherit;
  font-size: 100%;
  line-height: 1.15;
  margin: 0;
}
button,
input {
  overflow: visible;
}
input[type="text"] {
  border-radius: 0;
}
body {
  width: 100%;
  max-width: 68rem;
  margin: 0 auto;
  font:
    1.6rem/1.25 Arial,
    sans-serif;
  background-color: #f5f5f5;
  color: #4d4d4d;
}
@media screen and (min-width: 620px) {
  body {
    font-size: 1.9rem;
    line-height: 1.31579;
  }
}
/\* 重置样式结尾 \*/

/\* 全局样式 \*/
.form-group > input[type="text"] {
  display: inline-block;
  margin-top: 0.4rem;
}
.btn {
  padding: 0.8rem 1rem 0.7rem;
  border: 0.2rem solid #4d4d4d;
  cursor: pointer;
  text-transform: capitalize;
}
.btn.toggle-btn {
  border-width: 1px;
  border-color: #d3d3d3;
}
.btn.toggle-btn[aria-pressed="true"] {
  text-decoration: underline;
  border-color: #4d4d4d;
}
.btn\_\_danger {
  color: #fff;
  background-color: #ca3c3c;
  border-color: #bd2130;
}
.btn\_\_filter {
  border-color: lightgrey;
}
.btn\_\_primary {
  color: #fff;
  background-color: #000;
}
.btn\_\_primary:disabled {
  color: darkgrey;
  background-color: #565656;
}
.btn-group {
  display: flex;
  justify-content: space-between;
}
.btn-group > \* {
  flex: 1 1 49%;
}
.btn-group > \* + \* {
  margin-left: 0.8rem;
}
.label-wrapper {
  margin: 0;
  flex: 0 0 100%;
  text-align: center;
}
.visually-hidden {
  position: absolute !important;
  height: 1px;
  width: 1px;
  overflow: hidden;
  clip: rect(1px 1px 1px 1px);
  clip: rect(1px, 1px, 1px, 1px);
  white-space: nowrap;
}
[class\*="stack"] > \* {
  margin-top: 0;
  margin-bottom: 0;
}
.stack-small > \* + \* {
  margin-top: 1.25rem;
}
.stack-large > \* + \* {
  margin-top: 2.5rem;
}
@media screen and (min-width: 550px) {
  .stack-small > \* + \* {
    margin-top: 1.4rem;
  }
  .stack-large > \* + \* {
    margin-top: 2.8rem;
  }
}
.stack-exception {
  margin-top: 1.2rem;
}
/\* 全局样式结尾 \*/

.todoapp {
  background: #fff;
  margin: 2rem 0 4rem 0;
  padding: 1rem;
  position: relative;
  box-shadow:
    0 2px 4px 0 rgb(0 0 0 / 20%),
    0 2.5rem 5rem 0 rgb(0 0 0 / 10%);
}
@media screen and (min-width: 550px) {
  .todoapp {
    padding: 4rem;
  }
}
.todoapp > \* {
  max-width: 50rem;
  margin-left: auto;
  margin-right: auto;
}
.todoapp > form {
  max-width: 100%;
}
.todoapp > h1 {
  display: block;
  max-width: 100%;
  text-align: center;
  margin: 0;
  margin-bottom: 1rem;
}
.label\_\_lg {
  line-height: 1.01567;
  font-weight: 300;
  padding: 0.8rem;
  margin-bottom: 1rem;
  text-align: center;
}
.input\_\_lg {
  padding: 2rem;
  border: 2px solid #000;
}
.input\_\_lg:focus {
  border-color: #4d4d4d;
  box-shadow: inset 0 0 0 2px;
}
[class\*="\_\_lg"] {
  display: inline-block;
  width: 100%;
  font-size: 1.9rem;
}
[class\*="\_\_lg"]:not(:last-child) {
  margin-bottom: 1rem;
}
@media screen and (min-width: 620px) {
  [class\*="\_\_lg"] {
    font-size: 2.4rem;
  }
}
.filters {
  width: 100%;
  margin: unset auto;
}
/\* 待办条目样式 \*/
.todo {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
}
.todo > \* {
  flex: 0 0 100%;
}
.todo-text {
  width: 100%;
  min-height: 4.4rem;
  padding: 0.4rem 0.8rem;
  border: 2px solid #565656;
}
.todo-text:focus {
  box-shadow: inset 0 0 0 2px;
}
/\* 复选框样式 \*/
.c-cb {
  box-sizing: border-box;
  font-family: Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  font-weight: 400;
  font-size: 1.6rem;
  line-height: 1.25;
  display: block;
  position: relative;
  min-height: 44px;
  padding-left: 40px;
  clear: left;
}
.c-cb > label::before,
.c-cb > input[type="checkbox"] {
  box-sizing: border-box;
  top: -2px;
  left: -2px;
  width: 44px;
  height: 44px;
}
.c-cb > input[type="checkbox"] {
  -webkit-font-smoothing: antialiased;
  cursor: pointer;
  position: absolute;
  z-index: 1;
  margin: 0;
  opacity: 0;
}
.c-cb > label {
  font-size: inherit;
  font-family: inherit;
  line-height: inherit;
  display: inline-block;
  margin-bottom: 0;
  padding: 8px 15px 5px;
  cursor: pointer;
  touch-action: manipulation;
}
.c-cb > label::before {
  content: "";
  position: absolute;
  border: 2px solid currentColor;
  background: transparent;
}
.c-cb > input[type="checkbox"]:focus + label::before {
  border-width: 4px;
  outline: 3px dashed #228bec;
}
.c-cb > label::after {
  box-sizing: content-box;
  content: "";
  position: absolute;
  top: 11px;
  left: 9px;
  width: 18px;
  height: 7px;
  transform: rotate(-45deg);
  border: solid;
  border-width: 0 0 5px 5px;
  border-top-color: transparent;
  opacity: 0;
  background: transparent;
}
.c-cb > input[type="checkbox"]:checked + label::after {
  opacity: 1;
}
```

当我们为标记语言加上样式后，现在看起来好多了：

![我们的待办事项列表应用程序，有样式，标题为“what needs to be done”，用于输入更多待办事项的输入框以及带有复选框的待办事项列表](/zh-CN/docs/Learn/Tools_and_testing/Client-side_JavaScript_frameworks/Svelte_Todo_list_beginning/05-styled-todo-app.png)

到目前为止的代码
--------

### Git

若想要看到本文结束后程序代码所呈现的最终结果，请获取我们仓库的复制：

```bash
cd mdn-svelte-tutorial/03-adding-dynamic-behavior
```

或直接下载文件夹内容：

```bash
npx degit opensas/mdn-svelte-tutorial/03-adding-dynamic-behavior
```

记得执行 `npm install && npm run dev` 以开发模式来运行你的应用程序。

### REPL

若要在 REPL 查看当前程序代码状态，请点击如下链接：

https://svelte.dev/repl/c862d964d48d473ca63ab91709a0a5a0?version=3.23.2

总结
--

随着标记和样式设计的到位，我们的待办事项列表应用程序已初具雏形，一切准备就绪，我们可以开始专注于需要实现的特性了。

* 上一页
* Overview: Client-side JavaScript frameworks
* 下一页


Angular 入门 - 学习 Web 开发

Angular 入门
==========

* 上一页  (en-US)
* Overview: Client-side JavaScript frameworks
* 下一页  (en-US)

是时候将目光转向谷歌的 Angular 框架了，这是一种你可能经常遇到的选项。在这篇文章中我们关注 Angular 能提供什么、安装必备的组件、建立一个示例应用，并关注 Angular 的基本架构。

|  |  |
| --- | --- |
| 前提： | 熟悉核心的 HTML、CSS 和 JavaScript 语言，以及终端命令行的知识。 |
| 目标： | 要设置本地 Angular 的开发环境，创建一个新手应用，并理解它工作原理的基础知识。 |

Angular 是什么？
------------

Angular 是一个开发平台，基于 TypeScript。作为一个平台，Angular 包含了：

* 基于组件的框架，可用于构建可扩展的 Web 应用程序
* 集成良好的库（library），涵盖各种功能，包括路由、表单管理、客户端——服务器通信等
* 开发人员工具，可帮助你开发、构建、测试和更新代码

当你用 Angular 搭建一个应用，你正在利用一个可以从单开发人员项目扩展到企业级应用程序的平台。Angular 被设计用来使版本迭代尽可能简单。因此，你可以毫不费力地利用最新成果（developments）。最为重要的是，Angular 的生态系统由多达 170 万的开发人员、库（library）作者和内容创作者构成的多元化群体组成。

在你开始探索 Angular 平台之前，你需要了解 Angular 的命令行界面（CLI）。Angular 的命令行界面是最快、最简单、也最值得推荐的开发 Angular 应用的方法。Angular 的命令行界面使许多任务变得简单。以下是一些示例：

|  |  |
| --- | --- |
| `ng build` | 将 Angular 应用程序或库编译到给定的输出目录中。 |
| `ng serve` | 构建你的应用并启动开发服务器，当有文件变化时就重新构建。 |
| `ng generate` | 基于某个原理图生成或修改文件。 |
| `ng test` | 在给定项目中运行单元测试。 |
| `ng e2e` | 构建一个 Angular 应用并启动开发服务器，然后运行端到端测试。 |

你将会发现 Angular 的命令行界面是一个用于构建 Angular 应用程序的宝贵工具。

你将构建什么？
-------

本教程系列将指导你完成构建待办事项列表应用程序的过程。通过这个应用程序，你将学习如何使用 Angular 来管理、编辑、添加、删除和过滤项目。

必备条件
----

为了在你的本地系统中安装 Angular，你需要：

* **Node.js**
 Angular 需要一个 Node.js 的目前活跃的（active）长期支持版本（LTS）或者一个维护的（maintenance）长期支持版本。有关特定版本要求的信息，参见在 package.json 文件中的 `engines` 值。更多安装 Node.js 的信息，参见 nodejs.org。如果你不确定在系统上运行哪个版本的 Node.js，请在终端窗口中运行 `node -v`。
* **npm 包管理器**
 Angular、Angular 的命令行界面和 Angular 应用依赖 npm 包以实现许多特性和功能。若要下载安装 npm 包，就需要一个 npm 包管理器。这个教程使用在安装 `Node.js` 时默认安装的 npm 客户端命令行界面。若要检查是否已安装 npm 客户端，请在终端窗口中运行 `npm -v`。
设置你的应用程序
--------

你可以使用 Angular 命令行界面来运行用于生成、构建、测试和部署 Angular 应用程序终端的命令。为了安装 Angular 命令行界面，在你的终端运行以下命令：

```bash
npm install -g @angular/cli
```

Angular 命令行界面命令均以 `ng` 开头，其后是你希望命令行界面执行的操作。在桌面目录中，使用以下 `ng new` 命令创建一个名为 `todo` 的应用程序：

```bash
ng new todo --routing=false --style=css
```

`ng new` 命令会在桌面上创建一个最小的 Angular 初始应用程序。附加属性 `--routing` 和 `--style` 规定了如何处理应用程序中的导航和样式。

本教程稍后将会更详细地介绍这些功能。如果系统提示你强制执行更严格的类型检查，则可以使用 yes 进行响应。

使用以下 `cd` 命令导航到新项目：

```bash
cd todo
```

要运行 `todo` 应用程序，请使用 `ng serve`：

```bash
ng serve
```

当命令行界面提示你有关分析的信息时，请回答 `no`。

在浏览器中，导航到 `http://localhost:4200/` 以查看新的初始应用程序。如果你更改了任何源文件，应用程序都将自动重新加载。

当 `ng serve` 命令正在运行时，你可能希望打开另一个终端选项卡或窗口以运行新命令。如果你在任何时候想要停止运行应用程序，请在终端中按 `Ctrl + c`。

熟悉你的 Angular 应用程序
-----------------

本教程重点介绍的应用程序源文件位于 `src/app` 中。命令行界面自动生成的关键文件包含：

1. `app.module.ts`：指定应用程序使用的文件。
 此文件充当应用程序中其他文件的中心枢纽。
2. `app.component.ts`：也称为类，包含着应用程序主页的逻辑。
3. `app.component.html`：包含 `AppComponent` 的 HTML。此文件的内容也被称为模板。
 模板将确定视图或你在浏览器中看到的内容。
4. `app.component.css`: 包含 `AppComponent` 的样式。如果要定义仅适用于特定组件的样式（而不是整个应用程序），则可以使用此文件。

Angular 中的组件由三个主要部分组成：模板、样式和类。例如，`app.component.ts`、`app.component.html` 和 `app.component.css` 共同构成了 `AppComponent`。此结构将逻辑、视图和样式分开，使应用程序更易于维护和缩放。

通过这种方式，你从一开始就使用了最佳实践。

Angular 的命令行界面同时也生成一个叫作 `app.component.spec.ts` 的用于组件测试的文件。但本教程不会进行测试，因此你可以忽略这个文件。

每当你生成一个组件时，命令行界面都会使用你指定的名称在目录中创建这四个文件。

Angular 应用程序的结构
---------------

Angular 用 TypeScript 构建。TypeScript 是 JavaScript 的超集，这意味着任何有效的 JavaScript 都是有效的 TypeScript。TypeScript 提供了比普通 JavaScript 类型推断和更简洁的语法，它为你提供了一个工具，用于创建更易于维护的代码并最大限度地减少错误。

组件是 Angular 应用程序的构建块。组件包括一个 TypeScript 类，该类具有 `@Component()` 修饰器、HTML 模板和样式。

### 类（class）

 类是放置组件所需的任何逻辑的位置。这种代码可以包括函数、事件监听器、属性和对服务的引用等。类位于名称为 `feature.component.ts` 的文件中，其中 `feature` 为组件的名称。因此，你可以有命名类似 `header.component.ts`、`signup.component.ts` 或 `feed.component.ts` 的文件。
 你创建一个带有 `@Component()` 修饰器的组件，该装饰器具有元数据，该元数据告诉 Angular 在哪里可以找到 HTML 和 CSS。典型组件如下：

```js
import { Component } from "@angular/core";

@Component({
  selector: "app-item",
  // the following metadata specifies the location of the other parts of the component
  templateUrl: "./item.component.html",
  styleUrls: ["./item.component.css"],
})
export class ItemComponent {
  // your code goes here
}
```

此组件被称为 `ItemComponent`，其选择器为 `app-item`。你可以通过将选择器放置在其他模板中来像使用常规 HTML 标记一样使用它。当选择器位于模板中时，浏览器将渲染该组件的模板。

本教程将指导你创建两个组件并在一个组件中使用另一个组件。Angular 的组件模型提供了强大的封装和直观的应用程序结构。组件还使应用程序更易于进行单元测试，并且可以提高代码的整体可读性。

### HTML 模板

每个组件都会有一个 HTML 模板，用于声明该组件的渲染方式。你可以内联或按文件路径定义此模板。

若要引用外部 HTML 文件，请使用 `templateUrl` 属性：

```js
@Component({
  selector: "app-root",
  templateUrl: "./app.component.html",
})
export class AppComponent {}
```

要编写内联 HTML，请使用 `template` 属性并在反引号中编写 HTML：

```js
@Component({
  selector: "app-root",
  template: `<h1>Hi!</h1>`,
})
export class AppComponent {}
```

Angular 使用额外的语法扩展 HTML，允许你从组件中插入动态值。Angular 会在组件的状态更改时自动更新渲染的文档对象模型（DOM）。此功能的一个用途是插入动态文本，如下面的示例所示。

```html
<h1>{{ title }}</h1>
```

双大括号指示 Angular 插入其中的内容。`title` 的值来自组件类：

```js
import { Component } from "@angular/core";

@Component({
  selector: "app-root",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"],
})
export class AppComponent {
  title = "To do application";
}
```

当应用程序加载组件及其模板时，你将在浏览器将看到以下内容：

```html
<h1>To do application</h1>
```

### 样式

组件可以从应用程序的 `styles.css` 文件中继承全局样式，并使用自己的样式扩充或覆盖它们。你可以直接在 `@Component()` 修饰器中编写特定于组件的样式，也可以指定 CSS 文件的路径。

若要将样式直接包含在组件修饰器中，请使用 `styles` 属性：

```js
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styles: ['h1 { color: red; }']
})
```

通常，组件会使用 `styleUrls` 属性在单独的文件中使用样式：

```js
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
```

使用特定于组件的样式，你可以组织 CSS，使其易于维护和移植。

总结
--

这就是我们给你的第一次关于 Angular 的介绍。此时，你应该已经设置并准备好构建 Angular 应用程序，并对 Angular 的工作原理有基本的了解。在下一篇文章中，我们将加深这些知识，并开始构建待办事项列表应用程序的结构。

* 上一页  (en-US)
* Overview: Client-side JavaScript frameworks
* 下一页  (en-US)


渲染 Vue 组件列表 - 学习 Web 开发

渲染 Vue 组件列表
===========

* 上一页
* Overview: Client-side JavaScript frameworks
* 下一页

现在我们已经有了一个可以工作的组件。接下来我们将添加更多的 `ToDoItem` 组件到我们的应用中。本文我们会添加一系列待办事项到 `App.vue` 组件并使用 `v-for` 指令遍历它们，将它们的每一项展示在 `ToDoItem` 组件中。

|  |  |
| --- | --- |
| 预备条件： | 熟悉核心 HTML、CSS 和 JavaScript 语言，了解终端/命令行知识。

 Vue 组件是由管理应用程序数据的 JavaScript 对象和映射到基础 DOM
 结构的基于 HTML 的模板语法组成的。为了进行安装并使用 Vue
 的一些更高级的功能（例如“单文件组件（SFC）”或渲染功能），你将需要一个装有
 node + npm 的终端。
  |
| 目标： | 了解如何循环浏览一个数据数组，并在多个组件中渲染。 |

利用 v-for 指令渲染列表
---------------

一个有效的待办事项列表需要有多个被渲染的 to-do 项，Vue 中的 `v-for` 可以用来实现这种效果。它是 Vue 自带的指令，用于在 template 中实现循环，我们可以利用它将数组中的各项重复渲染成指定的特征。我们将利用它迭代待办事项列表，将其中的每一项展示为单独的 `ToDoItem` 组件。

### 添加一些需要渲染的数据

首先我们需要准备一个待办事项数组。添加 `data` 属性到 `App.vue` 组件对象中，它包含一个 `ToDoItems` 字段，其值是待办事项数组。在最终完成添加新的待办事项功能之前，我们可以先 mock 一些待办项目，每个待办项目可以用一个对象表示，这个对象含有 `label` 和 `done` 属性。

像下面这样添加一些待办项目让我们可以利用 `v-for` 来对它们进行渲染。

```js
export default {
  name: "app",
  components: {
    ToDoItem,
  },
  data() {
    return {
      ToDoItems: [
        { label: "Learn Vue", done: false },
        { label: "Create a Vue project with the CLI", done: true },
        { label: "Have fun", done: true },
        { label: "Create a to-do list", done: false },
      ],
    };
  },
};
```

现在我们有了一个列表，可以用 `v-for` 去展示它们了。指令的作用方式和元素的属性类似，就 `v-for` 而言，它类似 JavaScript 中的 `for...in` 循环——`v-for="item in items"`——`items` 是你要迭代的列表，`item` 是数组中当前元素的引用。

`v-for` 获取每个迭代的元素，并渲染它和它的子元素。在我们的例子中，我们用 `<li>` 的形式展示每一个待办事项，接下来我们会通过每个待办事项传递数据到其对应的 `ToDoItem` 组件。

### Key 属性

在进行数据传递之前，我们要了解下 `key` 属性，它和 `v-for` 一起使用，用来帮助 Vue 标识列表中的元素，这样 Vue 就不需要在列表变化时重新创建它们。为了确保它适当地重新使用列表元素，它需要在你附加 `v-for` 的同一个元素上有一个独特的“key”。

为了确保 Vue 能够准确地比较 `key` 属性，它们需要是字符串或数字值。虽然使用名字字段很好，但这个字段最终将由用户输入控制，这意味着我们不能保证名字是唯一的。然而，我们可以使用 `lodash.uniqueid()`，就像我们在上一篇文章中做的那样。

1. 使用导入 `ToDoItem` 组件相同的方法导入 `lodash.uniqueid` 到 `App` 组件。

```js
import uniqueId from "lodash.uniqueid";
```

2. 添加 `id` 字段到 `ToDoItems` 数组的每一个元素中，并且将他们赋值为 `uniqueId('todo-')`。
 此时，`App.vue` 中的 `<script>` 元素内容应该如下：

```js
import ToDoItem from "./components/ToDoItem.vue";
import uniqueId from "lodash.uniqueid";
export default {
  name: "app",
  components: {
    ToDoItem,
  },
  data() {
    return {
      ToDoItems: [
        { id: uniqueId("todo-"), label: "Learn Vue", done: false },
        {
          id: uniqueId("todo-"),
          label: "Create a Vue project with the CLI",
          done: true,
        },
        { id: uniqueId("todo-"), label: "Have fun", done: true },
        { id: uniqueId("todo-"), label: "Create a to-do list", done: false },
      ],
    };
  },
};
```

3. 在你的 `App.vue` 模板中添加 `v-for` 指令和 `key` 属性到 `<li>` 元素：

```html
<ul>
  <li v-for="item in ToDoItems" :key="item.id">
    <to-do-item label="My ToDo Item" :done="true"></to-do-item>
  </li>
</ul>
```

这样修改后，`<li>` 标签中的 JavaScript 表达式就可以访问 `item` 了，这意味着我们可以使用 `v-bind` 来传递 `item` 对象的字段给 `ToDoItem` 组件了。这非常有用，我们想让列表中的待办事项的 `label` 值展示到它的 label 中，而不是显示一个静态的“My Todo Item”。此外，我们想让它们的 checked 状态反映它们的 `done` 字段，而不是总设置为 `done="true"`。
4. 把 `label="My ToDo Item"` 改成 `:label="item.label"`, `:done="false"` 改成 `:done="item.done"`，像下面这样：

```html
<ul>
  <li v-for="item in ToDoItems" :key="item.id">
    <to-do-item :label="item.label" :done="item.done"></to-do-item>
  </li>
</ul>
```

现在当你去看运行着的 app 时，你会发现待办事项显示了它们自己正确的名字，如果你查看源码的话，你会发现输入都有了唯一的 `id`，它取自 `App` 组件中的对象。

![含有已渲染的待办事项的应用](/zh-CN/docs/Learn/Tools_and_testing/Client-side_JavaScript_frameworks/Vue_rendering_lists/rendered-todo-items.png)

让我们来一点小重构
---------

在这里我们可以做一个小小的重构。我们可以把 `id` 变成一个 prop，而不是在 `ToDoItem` 组件中为复选框生成它。虽然这不是严格意义上的需要，但它使我们更容易管理，因为我们已经需要为每个 todo 项目创建一个唯一的 `id`。

1. 添加一个新的 prop `id` 到 `ToDoItem` 组件。
2. 标记它为 required，类型是 `String`。
3. 为防止命名冲突，删除掉 `data` 属性中的 `id` 字段。
4. 现在不需要再使用 `uniqueId` 了，所以需要删除掉 `import uniqueId from 'lodash.uniqueid';` 这行，否则你的应用会报错。

现在，`ToDoItem` 中的 `<script>` 内容应该如下所示：

```js
export default {
  props: {
    label: { required: true, type: String },
    done: { default: false, type: Boolean },
    id: { required: true, type: String },
  },
  data() {
    return {
      isDone: this.done,
    };
  },
};
```

现在，在 `App.vue` 组件中将 `item.id` 作为 prop 传递给 `ToDoItem` 组件。你的 `App.vue` template 应该如下所示：

```html
<template>
  <div id="app">
    <h1>My To-Do List</h1>
    <ul>
      <li v-for="item in ToDoItems" :key="item.id">
        <to-do-item
 :label="item.label"
 :done="item.done"
 :id="item.id"></to-do-item>
      </li>
    </ul>
  </div>
</template>
```

你渲染后的站点看起来是没有变化的，但是这次重构使得 `item.id` 像其他参数一样，作为 prop 从 `App.vue` 传递给 `ToDoItem`。现在代码变得更有逻辑性和一致性。

总结
--

整篇文章就到这里了。我们现在有了样例数据，然后我们用循环将每一项渲染成 `ToDoItem`。

接下来我们需要让用户可以输入它们自己的待办事项，想做到这一点，我们需要一个文本输入 `<input>`，当用户输入数据时触发事件，在事件响应函数中需要将数据添加到待办事项列表并且重新渲染列表，我们还需要一个模型操控数据。我们将在下一篇文章中获取这些知识。

* 上一页
* Overview: Client-side JavaScript frameworks
* 下一页


Vue 资源 - 学习 Web 开发

Vue 资源
======

* 上一页
* Overview: Client-side JavaScript frameworks
* 下一页

现在，我们将为我们的 Vue 学习画上句号，这里将给你一个资源清单，你可以用它来进一步学习，还有一些其他有用的提示。

|  |  |
| --- | --- |
| 预备条件： |
 熟悉核心 HTML、CSS 和 JavaScript
 语言，了解终端/命令行知识。

Vue 组件是由管理应用程序数据的 JavaScript 对象和映射到基础 DOM 结构的基于 HTML 的模板语法组成的。为了进行安装并使用 Vue 的一些更高级的功能（例如“单文件组件（SFC）”或渲染功能），你将需要一个装有 node + npm 的终端。 |
| 目标： | 要了解去哪里寻找关于 Vue 的进一步信息，以继续学习。 |

更多资源
----

这里可以学到更多 Vue 知识：

* Vue 文档——Vue 网站主页，包含全面的文档，包括示例、指导手册和参考资料。这是开始深入学习 Vue 的最好地方。
* Vue Github 仓库——Vue 源码仓库。在这里你可以报告问题和/或直接对 Vue 代码库做出贡献。研究 Vue 的源代码可以帮助你更好地理解该框架的工作原理，并写出更好的代码。
* Vue 论坛——获取帮助的 Vue 官方论坛。
* Vue CLI 文档——Vue CLI 文档。这包含了关于定制和扩展你通过 CLI 生成的输出的信息。
* NuxtJS——NuxtJS 是服务器端 Vue 框架，它的一些架构意见对创建可维护的应用程序很有用，即使你不使用它提供的任何服务器端渲染功能。这个网站提供了关于使用 NuxtJS 的详细文档。
* Vue Mastery——一个 Vue 付费学习平台，也包含一些免费课程。
* Vue School——也是一个 Vue 付费学习平台。
构建并发布你的 Vue app
---------------

Vue CLI 还为我们提供了准备向网络发布应用程序的工具。你可以像这样做：

* 如果你的本地服务仍然正在运行，在控制台按下 `Ctrl` + `C` 停止它。
* 之后，在控制台运行 `npm run build`（或者 `yarn build`）。

这将创建一个新的 `dist` 目录，包含所有准备好的生产文件。要把你的网站发布到网上，请把这个文件夹的内容复制到你的主机环境中。

**备注：** Vue CLI 文档也包含一个专门的指导手册，可以帮助你将应用发布到很多常见的托管平台。

Vue 2
-----

Vue 2 的支持将于 2023 年 12 月 31 日结束，所有 CLI 工具的默认 Vue 版本为 3 及以上版本。组合式 API作为基于属性的 API 的替代，`setup()` 函数将作用于组件上。只有你从这个函数返回的东西在你的 `<template>` 中可用。在使用这个 API 时，你需要对“响应式”的属性进行明确说明。Vue 使用选项式 API为你处理这个问题。这使得新的 API 通常被认为是一个更高级的用例。

如果要从 Vue 2 升级，建议看一下 Vue 3 迁移指南。

* 上一页
* Overview: Client-side JavaScript frameworks
* 下一页


框架的主要特性 - 学习 Web 开发

框架的主要特性
=======

* 上一页
* Overview: Client-side JavaScript frameworks
* 下一页

每种主流的 JavaScript 框架都有自己不同的方式更新 DOM、处理浏览器事件并为开发者提供愉快的使用体验，这篇文章将探索“四大”框架的主要特性，从较高的层次探讨框架的工作方式以及它们之间的区别。

|  |  |
| --- | --- |
| 预备条件： | 熟悉 HTML、CSS 和 JavaScript 语言的核心知识。 |
| 目标： | 了解框架主要代码的特性。 |

领域特定语言
------

本模块中讨论的所有框架都基于 JavaScript，并且都允许你使用领域特定语言（DSL）来构建应用程序。特别地，React 已经普及了使用 **JSX** 来编写其组件，而 Ember 则使用 **Handlebars**。与 HTML 不同，这些语言知道如何读取数据变量，并且此数据可用于简化编写 UI 的过程。

基于 Angular 框架的应用程序会大量地使用 **TypeScript**。TypeScript 不关心用户界面的编写，但它仍然是一种领域特定语言（DSL），而且与原生 JavaScript 有着很大的不同。

DSL 不能直接由浏览器解析；它们首先要被转换成 JavaScript 或 HTML。虽然转换在开发流程中是一个额外的步骤，但是框架通常会包含处理此步骤所需的工具，或者与该步骤相适应。虽然你可以不使用这些领域特定语言构建框架应用程序，但使用它们会简化你的开发过程，还能更容易地从这些框架的周边社区中找到帮助。

### JSX

JSX 代表 JavaScript 和 XML，是 JavaScript 的扩展，为 JavaScript 环境带来了类似 HTML 的语法。它由 React 团队发明，用于 React 应用程序，但也可用于开发其他应用程序——例如 Vue 应用程序。

这是一个简单的 JSX 示例：

```js
const subject = "World";
const header = (
  <header>
    <h1>Hello, {subject}!</h1>
  </header>
);
```

小括号内的表达式表示一个 HTML `<header>` 元素，里面还有一个 `<h1>` 元素。第 4 行 `subject` 周围的花括号告诉应用程序要读取常量 `subject` 的值并将其插入到我们的 `<h1>` 元素当中。

当与 React 一起使用时，上一个片段中的 JSX 将被编译成这样：

```js
const subject = "World";
const header = React.createElement(
  "header",
  null,
  React.createElement("h1", null, "Hello, ", subject, "!"),
);
```

当最终被浏览器渲染时，上述片段将产生如下的 HTML 内容：

```html
<header>
  <h1>Hello, World!</h1>
</header>
```

### Handlebars

Handlebars 是一种简单的模板语言，虽然它并非仅可用于 Ember 应用程序，但它在 Ember 应用程序中被大量使用。Handlebars 代码类似于 HTML，但它可以选择从其他地方获取数据。这些数据可用于影响应用程序最终构建的 HTML。

与 JSX 相似的是，Handlebars 使用花括号来注入变量的值。不相似的是，Handlebars 使用双花括号，而不是单花括号。

下面是一个简单的 Handlebars 示例：

给定以下 Handlebars 模板：

```html
<header>
  <h1>Hello, {{subject}}!</h1>
</header>
```

还有以下数据：

```js
{
  subject: "World";
}
```

Handlebars 将像这样构建 HTML：

```html
<header>
  <h1>Hello, World!</h1>
</header>
```

### TypeScript

TypeScript 是 JavaScript 的*超集*，这意味着它*扩展*了 JavaScript——所有 JavaScript 代码都是有效的 TypeScript 代码，但反之则不然。TypeScript 可以让开发者们“严格地”执行他们的代码。例如，要写一个函数 `add()`，它接受整数 `a` 和 `b` 并返回它们的总和。

在 JavaScript 中，可以这样编写：

```js
function add(a, b) {
  return a + b;
}
```

对于习惯 JavaScript 的人来说，这段代码非常简单明确，但它仍然可以更加清晰明确。JavaScript 允许我们使用 `+` 运算符将字符串连接在一起，所以，如果传入 `add()` 函数的 `a` 和 `b` 是字符串，这个函数在技术上仍然可以工作——但是它可能不会给你期望的结果。如果我们只想允许将数字传递给这个函数怎么办？可以使用 TypeScript：

```js
function add(a: number, b: number) {
  return a + b;
}
```

在 `a` 和 `b` 后面的 `: number` 代码告诉 TypeScript `a` 和 `b` 都必须是数字。这样的话，当我们再次将字符串（例如 `'2'`）传入这个函数的时候，TypeScript 就会在编译期间报告错误，而我们将被迫修复这个错误。虽然可以编写自己的 JavaScript 来做到与 TypeScript 一样的效果，但是这会使我们的写的代码更加繁杂。让 TypeScript 为我们处理此类检查或许更有意义。

编写组件
----

如前一章所述，大多数框架都有某种组件模型。React 组件可以使用 JSX 编写，Ember 组件可以使用 Handlebars 编写，Angular 和 Vue 组件可以使用模板语法轻松地扩展 HTML。

不管框架的作者们对于如何编写组件的看法有多么不同，每个框架的组件都提供了一种方法来描述组件可能需要的外部属性、组件应该管理的内部状态以及用户在组件上可以触发的事件。

本节其余部分的代码片段将使用 React 作为示例，并使用 JSX 编写其组件。

### 属性

属性（**props**）是渲染组件所需要的外部数据。假设你正在为一个在线杂志建立网站，你需要确保每一个撰稿人都能得到他们工作的荣誉。你可能会创建一个 `AuthorCredit` 组件，与每篇文章一起使用。这个组件需要显示作者的头像和关于他们的简短的介绍。为了知道渲染什么样的图片，打印什么样的介绍，`AuthorCredit` 需要接受一些属性。

这个 `AuthorCredit` 组件的 React 写法可能看起来像这样：

```js
function AuthorCredit(props) {
  return (
    <figure>
      <img src={props.src} alt={props.alt} />
      <figcaption>{props.byline}</figcaption>
    </figure>
  );
}
```

`{props.src}`、`{props.alt}` 和 `{props.byline}` 代表我们将插入组件中属性的位置。为了渲染这个组件，我们会在我们想要渲染它的地方（可能是在另一个组件里面）写下这样的代码：

```js
<AuthorCredit
  src="./assets/zelda.png"
  alt="Portrait of Zelda Schiff"
  byline="Zelda Schiff is editor-in-chief of the Library Times."
/>
```

这将最终在浏览器中呈现以下 `<figure>` 元素，其结构由 `AuthorCredit` 组件定义，其内容由 `AuthorCredit` 组件调用中的 props 定义：

```html
<figure>
  <img src="assets/zelda.png" alt="Portrait of Zelda Schiff" />
  <figcaption>Zelda Schiff is editor-in-chief of the Library Times.</figcaption>
</figure>
```

### 状态

我们在上一章谈到了**状态**的概念，强大的状态处理机制是高效框架的关键，每个组件都可能有需要控制其状态的数据。只要组件还在使用，这种状态就会以某种方式持续存在。像属性一样，状态可以用来影响组件的渲染方式。

作为一个示例，考虑一个计算它被点击了多少次的按钮。这个组件应该负责跟踪自己的 *count* 状态，可以这样写：

```js
function CounterButton() {
  const [count] = useState(0);
  return <button>Clicked {count} times</button>;
}
```

`useState()` 是一种 **React 钩子**，根据给定初始值，跟踪其更新时的变化。代码最初在浏览器会渲染成下面这样：

```html
<button>Clicked 0 times</button>
```

`useState()` 调用会在整个应用中全程稳定跟踪 `count` 的值，你不需要再自行撰写代码处理这些。

### 事件

为了实现交互性，组件需要对浏览器事件做出响应，这样我们的应用程序才能对用户做出响应。每个框架都为监听浏览器事件提供了自己的语法，这些语法参考了相应的浏览器原生事件的名称。

在 React 中，监听 `click` 事件需要一个特殊的属性 `onClick`。让我们更新上述 `CounterButton` 代码，允许它为每次点击计数：

```js
function CounterButton() {
  const [count, setCount] = useState(0);
  return (
    <button onClick={() => setCount(count + 1)}>Clicked {count} times</button>
  );
}
```

在这个版本中，我们使用额外的 `useState()` 功能来创建一个特殊的 `setCount()` 函数，我们可以调用它来更新 `count` 的值。我们在第 4 行调用这个函数，并将 `count` 设置为其当前值加 1。

为组件添加样式
-------

每个框架都提供了一种为你的组件或整个应用程序定义样式的方法。尽管每个框架定义组件样式的方法略有不同，但它们都给了你多种方式。通过添加一些辅助模块，你可以用 Sass 或 Less 来为你的框架应用程序设计样式，或者用 PostCSS 来转译你的 CSS 样式表。

处理依赖
----

所有主要的框架都提供了处理依赖关系的机制——在其他组件内使用组件，有时还会涉及到多层次。与其他功能一样，不同框架的确切机制会有所不同，但最终结果是一样的。组件倾向于使用标准的 JavaScript 模块语法将组件导入其他组件，或者至少是类似的东西。

### 组件嵌套

基于组件的用户界面架构的一个关键好处是，组件可以被组合在一起。就像你可以把 HTML 标签写在彼此里面来建立一个网站一样，你可以在其他组件里面使用组件来建立一个网络应用。每个框架都允许你编写利用（并因此依赖）其他组件的组件。

例如，我们的 `AuthorCredit` React 组件可能被用于 `Article` 组件中。这意味着 `Article` 需要导入 `AuthorCredit`。

```js
import AuthorCredit from "./components/AuthorCredit";
```

一旦完成导入，就可以在 `Article` 组件中使用 `AuthorCredit` 组件：

```js
// …

<AuthorCredit />

// …
```

### 依赖注入

现实世界的应用往往涉及到多层次嵌套的组件结构。一个 `AuthorCredit` 组件嵌套了很多层，可能由于某些原因，需要来自我们应用程序根层的数据。

比方说，我们正在建立这样的杂志网站：

```js
<App>
  <Home>
    <Article>
      <AuthorCredit {/\* props \*/} />
    </Article>
  </Home>
</App>
```

我们的 `App` 组件有 `AuthorCredit` 组件需要的数据。我们可以重写 `Home` 和 `Article`，让它们向下传递属性，但如果我们的数据的来源和目的地之间有很多层，这就会变得很繁琐。这也是不合理的：`Home` 和 `Article` 实际上没有使用作者的肖像或署名，但如果我们想把这些信息放到 `AuthorCredit` 中，我们需要改变 `Home` 和 `Article` 来适应它。

通过许多层组件传递数据的问题被称为“属性穿透”，它对大型应用来说并不理想。

为了规避属性穿透，框架提供了依赖注入的功能，这是一种将某些数据直接传递给需要它的组件的方法，而不需要通过中间的层次。每个框架都以不同的名称和方式来实现依赖注入，但效果最终是一样的。

Angular 把这个过程称为依赖注入；Vue 有 `provide()` 和 `inject()` 组件方法；React 有一个 Context API；Ember 通过服务分享状态。

### 生命周期

在框架上下文中，一个组件的**生命周期**是一个组件从被追加到 DOM 然后被浏览器渲染（通常称为*挂载*）到从 DOM 中移除（通常称为*卸载*）所经历的一系列阶段的集合。每个框架对这些生命周期阶段的命名都不同，而且不是所有的框架都能让开发者访问相同的阶段。所有的框架都遵循相同的一般模型：它们允许开发者在组件*挂载*、*渲染*和*卸载*以及这之间的许多阶段执行某些动作。

*渲染*阶段是最需要了解的，因为它在用户与应用程序交互时重复的次数最多。每当浏览器需要渲染一些新的东西时，它就会运行，无论这些新的信息是对浏览器中的内容进行补充、删除，还是对现有内容进行编辑。

这个 React 组件的生命周期图提供了对这个概念的总体概述。

渲染元素
----

就像生命周期一样，框架对如何渲染应用程序采取了不同但相似的方法。所有的框架都会跟踪浏览器 DOM 的当前渲染版本，并且每个框架都会对 DOM 在应用程序中的组件重新渲染时应该如何变化做出略有不同的决定。因为框架为你做出了这些决定，所以你通常不会自己与 DOM 交互。这种远离 DOM 的抽象比自己更新 DOM 更复杂、更耗费内存，但如果没有它，框架就不能让你以它们所熟知的声明性方式编程。

**虚拟 DOM** 是一种方法，此时关于浏览器的 DOM 信息被存储在 JavaScript 内存中。你的应用程序更新这个 DOM 副本，然后将其与“真正的”DOM（为你的用户实际渲染的 DOM）进行比较，以决定要渲染什么。应用程序建立一个“diff“来比较更新的虚拟 DOM 和当前渲染的 DOM 之间的差异，并使用该差异将更新应用到真实 DOM。React 和 Vue 都利用虚拟 DOM 模型，但它们建立“diff”或渲染应用的逻辑并不完全相同。

你可以在 React 文档阅读到更多有关虚拟 DOM 的信息。

**增量式 DOM** 与虚拟 DOM 类似，它建立一个 DOM“diff”来决定渲染什么，但不同的是，它不在 JavaScript 内存中创建一个完整的 DOM 副本。它忽略了 DOM 中不需要被改变的部分。Angular 是本模块到目前为止讨论的唯一一个使用增量 DOM 的框架。

你可以在 Auth0 博客阅读到更多有关增量式 DOM 的信息。

**Glimmer VM** 是 Ember 独有的。它既不是虚拟 DOM，也不是增量 DOM；它是一个独立的过程，通过它，Ember 的模板被转换为一种“字节码”，比 JavaScript 更容易和更快地阅读。

路由
--

正如前一章中提到的，路由是网络体验的一个重要部分。为了避免在具有大量视图的足够复杂的应用程序中出现破碎的体验，本模块中涉及的每个框架都提供了一个库（或多个库），帮助开发人员在其应用程序中实现客户端路由。

测试
--

所有的应用程序都受益于覆盖测试，以确保你的软件继续以你期望的方式运行，Web 应用程序也不例外。每个框架的生态系统都提供工具，以促进测试的编写。测试工具并没有内置于框架本身，但用于生成框架应用程序的命令行界面工具使你能够访问适当的测试工具。

每个框架在其生态系统中都有广泛的工具，具有单元和集成测试的能力。

Testing Library 是为许多 JavaScript 环境准备的一套测试工具，包括 React、Vue 和 Angular。Ember 文档涵盖了 Ember 应用的测试。

这是在 React 测试库的帮助下为我们的 `CounterButton` 写的一个快速测试。它测试了一些东西，比如按钮的存在，以及按钮在被点击 0 次、1 次和 2 次后是否显示了正确的文本：

```js
import React from "react";
import { render, fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom/extend-expect";

import CounterButton from "./CounterButton";

it("Renders a semantic button with an initial state of 0", () => {
  const { getByRole } = render(<CounterButton />);
  const btn = getByRole("button");
  expect(btn).toBeInTheDocument();
  expect(btn).toHaveTextContent("Clicked 0 times");
});

it("Increments the count when clicked", () => {
  const { getByRole } = render(<CounterButton />);
  const btn = getByRole("button");
  fireEvent.click(btn);
  expect(btn).toHaveTextContent("Clicked 1 times");
  fireEvent.click(btn);
  expect(btn).toHaveTextContent("Clicked 2 times");
});
```

总结
--

现在，你应该对你在使用框架创建应用程序时要使用的实际语言、功能和工具有了更多的了解。我相信你一定很想去做一些编码工作，这就是你接下来要做的事情！在这一点上，你可以选择你想先开始学习哪个框架：

* React
* Ember (en-US)
* Vue
* Svelte
* Angular
* 上一页
* Overview: Client-side JavaScript frameworks
* 下一页


开始我们的 React 待办清单 - 学习 Web 开发

开始我们的 React 待办清单
================

* 上一页
* Overview: Client-side JavaScript frameworks
* 下一页

假设我们的任务是在 React 中创建一个概念验证——一个允许用户添加、编辑和删除他们想做的任务的应用程序，并且在不删除任务的情况下将其标记为完成。这篇文章将引导你完成基本的 `App` 组件结构和样式，为以后添加的单个组件定义和交互性做好准备。

**备注：** 如果你需要对照我们的版本检查你的代码，你可以在我们的 todo-react 仓库 中找到一个完成版本的 React 应用代码。关于运行中的实时版本，参见 https://mdn.github.io/todo-react-build/.

|  |  |
| --- | --- |
| 条件： |
 熟悉核心 HTML、CSS
 和 JavaScript 语言，了解终端/命令行。
  |
| 目标： | 介绍我们待办清单的案例，并获得基本的 `App` 结构和样式。 |

用户故事
----

在软件开发中，用户故事是从用户的角度出发的一个可操作的目标。在我们开始工作之前，定义用户故事将帮助我们集中精力开展工作。我们的应用程序应该满足以下的故事：

作为一名用户，我可以

* 读取任务清单。
* 使用鼠标或键盘，添加一个任务。
* 使用鼠标或键盘，将任何任务标记为已完成。
* 使用鼠标或键盘，删除任何任务。
* 使用鼠标或键盘，编辑任何任务。
* 查看一个特定的任务子集：所有的任务、仅活动中的任务，或只有已完成的任务。

我们将逐一处理这些故事。

开始前的清理
------

create-react-app 添加了一些我们在项目中完全不会用到的文件。

* 我们不打算写每个组件的样式表，所以首先从 `App.js` 的顶部删除 `App.css` 的导入。
* 我们也不打算使用 `logo.svg` 文件，所以也要删除这个导入。

然后，复制并粘贴以下命令到你的终端，删除一些不需要的文件。确保你是在应用程序的根目录下开始的！

```bash
# Move into the src directory of your project
cd src
# Delete a few files
rm -- App.test.js App.css logo.svg serviceWorker.js setupTests.js
# Move back up to the root of the project
cd ..
```

注意：

* 我们要删除的两个文件是用来测试应用程序的。我们在这里不涉及测试。
* 如果你停止了你的服务器来做上面提到的终端任务，你必须用 `npm start` 再次启动它。
项目启动代码
------

作为这个项目的起点，我们将提供两样东西。一个 `App()` 函数来代替你现在的函数，还有一些 CSS 来改变你的应用程序的样式。

### JSX

复制以下片段到你的剪贴板，然后粘贴到 `App.js` 中，使其取代现有的 `App()` 函数：

```js
function App(props) {
  return (
    <div className="todoapp stack-large">
      <h1>TodoMatic</h1>
      <form>
        <h2 className="label-wrapper">
          <label htmlFor="new-todo-input" className="label\_\_lg">
            What needs to be done?
          </label>
        </h2>
        <input
          type="text"
          id="new-todo-input"
          className="input input\_\_lg"
          name="text"
          autoComplete="off"
        />
        <button type="submit" className="btn btn\_\_primary btn\_\_lg">
          Add
        </button>
      </form>
      <div className="filters btn-group stack-exception">
        <button type="button" className="btn toggle-btn" aria-pressed="true">
          <span className="visually-hidden">Show </span>
          <span>all</span>
          <span className="visually-hidden"> tasks</span>
        </button>
        <button type="button" className="btn toggle-btn" aria-pressed="false">
          <span className="visually-hidden">Show </span>
          <span>Active</span>
          <span className="visually-hidden"> tasks</span>
        </button>
        <button type="button" className="btn toggle-btn" aria-pressed="false">
          <span className="visually-hidden">Show </span>
          <span>Completed</span>
          <span className="visually-hidden"> tasks</span>
        </button>
      </div>
      <h2 id="list-heading">3 tasks remaining</h2>
      <ul
        role="list"
        className="todo-list stack-large stack-exception"
        aria-labelledby="list-heading">
        <li className="todo stack-small">
          <div className="c-cb">
            <input id="todo-0" type="checkbox" defaultChecked={true} />
            <label className="todo-label" htmlFor="todo-0">
              Eat
            </label>
          </div>
          <div className="btn-group">
            <button type="button" className="btn">
              Edit <span className="visually-hidden">Eat</span>
            </button>
            <button type="button" className="btn btn\_\_danger">
              Delete <span className="visually-hidden">Eat</span>
            </button>
          </div>
        </li>
        <li className="todo stack-small">
          <div className="c-cb">
            <input id="todo-1" type="checkbox" />
            <label className="todo-label" htmlFor="todo-1">
              Sleep
            </label>
          </div>
          <div className="btn-group">
            <button type="button" className="btn">
              Edit <span className="visually-hidden">Sleep</span>
            </button>
            <button type="button" className="btn btn\_\_danger">
              Delete <span className="visually-hidden">Sleep</span>
            </button>
          </div>
        </li>
        <li className="todo stack-small">
          <div className="c-cb">
            <input id="todo-2" type="checkbox" />
            <label className="todo-label" htmlFor="todo-2">
              Repeat
            </label>
          </div>
          <div className="btn-group">
            <button type="button" className="btn">
              Edit <span className="visually-hidden">Repeat</span>
            </button>
            <button type="button" className="btn btn\_\_danger">
              Delete <span className="visually-hidden">Repeat</span>
            </button>
          </div>
        </li>
      </ul>
    </div>
  );
}
```

现在打开 `public/index.html`，把 `<title>` 元素的文本改为 `TodoMatic` 。这样，它将与我们应用程序顶部的 `<h1>` 相匹配。

```html
<title>TodoMatic</title>
```

当你刷新浏览器时，你应该看到类似这样的东西：

![todo-matic app, unstyled, showing a jumbled mess of labels, inputs, and buttons](/zh-CN/docs/Learn/Tools_and_testing/Client-side_JavaScript_frameworks/React_todo_list_beginning/unstyled-app.png)

它很难看，而且还没有功能，但这没关系——我们一会儿就会为它设计样式。首先，考虑一下我们的 JSX，以及它是如何与我们的用户故事相对应的。

* 我们有一个 `<form>` 元素，有一个 `<input type="text">` 用于写出一个新任务，还有一个按钮用于提交表单。
* 我们有一组按钮，用于筛选我们的任务。
* 我们有一个标题，用于告诉我们还有多少任务。
* 我们有三个任务，安排在一个无序的列表中。每项任务都是一个列表项（`<li>`），有编辑和删除的按钮，还有一个复选框可以将其勾选为完成。

这些表格将允许我们*添加*任务；按钮让我们可以对任务进行*筛选*；我们通过标题和列表*查看*任务。用于*编辑*任务的 UI 目前很缺乏。这没关系——我们以后再写。

### 无障碍功能

你可能会注意到这里有一些不寻常的属性。比如说：

```html
<button type="button" className="btn toggle-btn" aria-pressed="true">
  <span className="visually-hidden">Show </span>
  <span>all</span>
  <span className="visually-hidden"> tasks</span>
</button>
```

这里的 `aria-pressed` 告诉辅助技术（比如屏幕阅读器），该按钮可以有两种状态。`按压` 或 `未按压` 。可以把它们看作 `开` 和 `关` 。设置为 `true` 意味着该按钮默认是被按下的。

`visually-hidden` 这个类还没有效果，因为我们还没有包含任何 CSS。一旦我们把样式写好，任何带有这个类的元素都会被隐藏起来，视力正常的用户不会看到，而屏幕阅读器用户仍然可以使用——这是因为视力正常的用户不需要这些词；它们的存在是为了给没有额外视觉环境帮助的屏幕阅读器用户提供更多关于这个按钮的信息。

再往下，你可以找到我们的 `<ul>` 元素：

```html
<ul
 role="list"
 className="todo-list stack-large stack-exception"
 aria-labelledby="list-heading"></ul>
```

`role` 属性帮助辅助技术解释一个标签代表哪种元素。默认情况下，`<ul>` 被视为一个列表，但我们即将添加的样式将改变它。添加 `role` 将恢复 `ul` 元素的“列表”含义。如果你想了解更多关于为什么要这样做，你可以看看 Scott O'Hara's article, "Fixing Lists".

`aria-labelledby` 属性告诉辅助技术，我们把列表标题当作标签，用来描述它下面的列表的目的。这种关联能让列表包含更多信息，这可以帮助屏幕阅读器用户更好地理解它的目的。

最后，我们列表项中的标签和输入有一些 JSX 特有的属性：

```html
<input id="todo-0" type="checkbox" defaultChecked="{true}" />
<label className="todo-label" htmlFor="todo-0"> Eat </label>
```

`<input />` 标签中的 `defaultChecked` 属性告诉 React 最初要检查这个复选框。如果我们像在普通的 HTML 中那样使用 `checked`，React 会在浏览器控制台中记录一些与处理复选框事件有关的警告，这是我们不想要的。现在不用太担心这个问题——我们将在以后使用事件的时候讨论这个问题。

`htmlFor` 属性对应于 HTML 中使用的 `for` 属性。因为 `for` 是一个保留词，我们不能在 JSX 中使用 `for` 作为属性，所以 React 使用 `htmlFor` 代替。

说明：

* 要在 JSX 属性中使用布尔值（`true` 和 `false`），你必须用大括号把它们括起来。如果你写 `defaultChecked="true"`，`defaultChecked` 的值将是 `"true"`——一个字符串字面。记住——这实际上是 JavaScript，而不是 HTML!
* 在前面的代码片段中使用的 `aria-pressed` 属性的值是 `"true"`，因为 `aria-pressed` 不是像 `checked` 那样是一个真正的布尔属性。
### 实现样式

将下面的 CSS 代码粘贴到 `src/index.css` 中，使其取代当前的内容：

```css
/\* RESETS \*/
\*,
\*::before,
\*::after {
  box-sizing: border-box;
}
\*:focus {
  outline: 3px dashed #228bec;
  outline-offset: 0;
}
html {
  font: 62.5% / 1.15 sans-serif;
}
h1,
h2 {
  margin-bottom: 0;
}
ul {
  list-style: none;
  padding: 0;
}
button {
  border: none;
  margin: 0;
  padding: 0;
  width: auto;
  overflow: visible;
  background: transparent;
  color: inherit;
  font: inherit;
  line-height: normal;
  -webkit-font-smoothing: inherit;
  -moz-osx-font-smoothing: inherit;
  -webkit-appearance: none;
}
button::-moz-focus-inner {
  border: 0;
}
button,
input,
optgroup,
select,
textarea {
  font-family: inherit;
  font-size: 100%;
  line-height: 1.15;
  margin: 0;
}
button,
input {
  overflow: visible;
}
input[type="text"] {
  border-radius: 0;
}
body {
  width: 100%;
  max-width: 68rem;
  margin: 0 auto;
  font:
    1.6rem/1.25 Arial,
    sans-serif;
  background-color: #f5f5f5;
  color: #4d4d4d;
}
@media screen and (min-width: 620px) {
  body {
    font-size: 1.9rem;
    line-height: 1.31579;
  }
}
/\*END RESETS\*/
/\* GLOBAL STYLES \*/
.form-group > input[type="text"] {
  display: inline-block;
  margin-top: 0.4rem;
}
.btn {
  padding: 0.8rem 1rem 0.7rem;
  border: 0.2rem solid #4d4d4d;
  cursor: pointer;
  text-transform: capitalize;
}
.btn.toggle-btn {
  border-width: 1px;
  border-color: #d3d3d3;
}
.btn.toggle-btn[aria-pressed="true"] {
  text-decoration: underline;
  border-color: #4d4d4d;
}
.btn\_\_danger {
  color: #fff;
  background-color: #ca3c3c;
  border-color: #bd2130;
}
.btn\_\_filter {
  border-color: lightgrey;
}
.btn\_\_primary {
  color: #fff;
  background-color: #000;
}
.btn-group {
  display: flex;
  justify-content: space-between;
}
.btn-group > \* {
  flex: 1 1 49%;
}
.btn-group > \* + \* {
  margin-left: 0.8rem;
}
.label-wrapper {
  margin: 0;
  flex: 0 0 100%;
  text-align: center;
}
.visually-hidden {
  position: absolute !important;
  height: 1px;
  width: 1px;
  overflow: hidden;
  clip: rect(1px 1px 1px 1px);
  clip: rect(1px, 1px, 1px, 1px);
  white-space: nowrap;
}
[class\*="stack"] > \* {
  margin-top: 0;
  margin-bottom: 0;
}
.stack-small > \* + \* {
  margin-top: 1.25rem;
}
.stack-large > \* + \* {
  margin-top: 2.5rem;
}
@media screen and (min-width: 550px) {
  .stack-small > \* + \* {
    margin-top: 1.4rem;
  }
  .stack-large > \* + \* {
    margin-top: 2.8rem;
  }
}
.stack-exception {
  margin-top: 1.2rem;
}
/\* END GLOBAL STYLES \*/
.todoapp {
  background: #fff;
  margin: 2rem 0 4rem 0;
  padding: 1rem;
  position: relative;
  box-shadow:
    0 2px 4px 0 rgba(0, 0, 0, 0.2),
    0 2.5rem 5rem 0 rgba(0, 0, 0, 0.1);
}
@media screen and (min-width: 550px) {
  .todoapp {
    padding: 4rem;
  }
}
.todoapp > \* {
  max-width: 50rem;
  margin-left: auto;
  margin-right: auto;
}
.todoapp > form {
  max-width: 100%;
}
.todoapp > h1 {
  display: block;
  max-width: 100%;
  text-align: center;
  margin: 0;
  margin-bottom: 1rem;
}
.label\_\_lg {
  line-height: 1.01567;
  font-weight: 300;
  padding: 0.8rem;
  margin-bottom: 1rem;
  text-align: center;
}
.input\_\_lg {
  padding: 2rem;
  border: 2px solid #000;
}
.input\_\_lg:focus {
  border-color: #4d4d4d;
  box-shadow: inset 0 0 0 2px;
}
[class\*="\_\_lg"] {
  display: inline-block;
  width: 100%;
  font-size: 1.9rem;
}
[class\*="\_\_lg"]:not(:last-child) {
  margin-bottom: 1rem;
}
@media screen and (min-width: 620px) {
  [class\*="\_\_lg"] {
    font-size: 2.4rem;
  }
}
.filters {
  width: 100%;
  margin: unset auto;
}
/\* Todo item styles \*/
.todo {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
}
.todo > \* {
  flex: 0 0 100%;
}
.todo-text {
  width: 100%;
  min-height: 4.4rem;
  padding: 0.4rem 0.8rem;
  border: 2px solid #565656;
}
.todo-text:focus {
  box-shadow: inset 0 0 0 2px;
}
/\* CHECKBOX STYLES \*/
.c-cb {
  box-sizing: border-box;
  font-family: Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  font-weight: 400;
  font-size: 1.6rem;
  line-height: 1.25;
  display: block;
  position: relative;
  min-height: 44px;
  padding-left: 40px;
  clear: left;
}
.c-cb > label::before,
.c-cb > input[type="checkbox"] {
  box-sizing: border-box;
  top: -2px;
  left: -2px;
  width: 44px;
  height: 44px;
}
.c-cb > input[type="checkbox"] {
  -webkit-font-smoothing: antialiased;
  cursor: pointer;
  position: absolute;
  z-index: 1;
  margin: 0;
  opacity: 0;
}
.c-cb > label {
  font-size: inherit;
  font-family: inherit;
  line-height: inherit;
  display: inline-block;
  margin-bottom: 0;
  padding: 8px 15px 5px;
  cursor: pointer;
  touch-action: manipulation;
}
.c-cb > label::before {
  content: "";
  position: absolute;
  border: 2px solid currentcolor;
  background: transparent;
}
.c-cb > input[type="checkbox"]:focus + label::before {
  border-width: 4px;
  outline: 3px dashed #228bec;
}
.c-cb > label::after {
  box-sizing: content-box;
  content: "";
  position: absolute;
  top: 11px;
  left: 9px;
  width: 18px;
  height: 7px;
  transform: rotate(-45deg);
  border: solid;
  border-width: 0 0 5px 5px;
  border-top-color: transparent;
  opacity: 0;
  background: transparent;
}
.c-cb > input[type="checkbox"]:checked + label::after {
  opacity: 1;
}
```

保存并刷新浏览器，你的应用程序现在应该有正常的样式。

总结
--

现在，我们的待办清单应用程序终于看起来像一个真正的应用程序了！问题是：它实际上什么都没做。我们将在下一章开始解决这个问题。

* 上一页
* Overview: Client-side JavaScript frameworks
* 下一页


将我们的 Svelte 应用组件化 - 学习 Web 开发

将我们的 Svelte 应用组件化
=================

* 上一页
* Overview: Client-side JavaScript frameworks
* 下一页

在上一篇文章中，我们开始开发我们的待办事项列表应用。本文的主要目标是讲解如何将我们的应用拆分为可管理的组件，并在它们之间共享信息。我们将对应用进行组件化，并添加更多功能，以允许用户更新现有组件。

|  |  |
| --- | --- |
| 前提： |
 至少，建议你熟悉核心的
 HTML、CSS 和
 JavaScript
 语言，并了解终端/命令行的知识。

你需要安装了 Node.js 和 npm 的终端来编译和构建应用。 |
| 目标： | 学习如何将我们的应用拆分为组件并在它们之间共享信息。 |

与我们一起编写代码
---------

### Git

克隆 GitHub 仓库（如果尚未完成）：

```bash
git clone https://github.com/opensas/mdn-svelte-tutorial.git
```

然后进入当前应用程序状态：

```bash
cd mdn-svelte-tutorial/04-componentizing-our-app
```

或者直接下载文件夹的内容：

```bash
npx degit opensas/mdn-svelte-tutorial/04-componentizing-our-app
```

记得运行 `npm install && npm run dev` 以在开发模式下启动应用程序。

### REPL

要使用 REPL 跟随我们一起编码，请从以下链接开始：

https://svelte.dev/repl/99b9eb228b404a2f8c8959b22c0a40d3?version=3.23.2

将应用拆分为组件
--------

在 Svelte 中，应用程序由一个或多个组件组成。组件是可重用的、自包含的代码块，将 HTML、CSS 和 JavaScript 组合在一起，写入 `.svelte` 文件中。组件可大可小，但通常具有明确定义：最有效的组件只服务于某个明确的目的。

定义组件的好处类似于将代码组织成可管理的片段的更一般的最佳实践。它将帮助你理解它们之间的关系，促进重用，并使你的代码更易于理解、维护和扩展。

但是，你如何知道什么应该拆分为组件呢？

这没有硬性规定。有些人更喜欢直观的方法，开始查看标记并在每个似乎具有自己逻辑的组件和子组件周围绘制封装。

其他人借鉴了一些用于创建新函数或对象的技术。其中一种技术是单一职责原则——也就是说，一个组件理想情况下只做一件事。如果它变得越来越庞大，应该将其拆分为更小的子组件。

这两种方法应相互补充，并帮助你决定如何更好地组织你的组件。

最终，我们将应用拆分为以下组件：

* `Alert.svelte`：用于通知已发生的操作的通用通知框。
* `NewTodo.svelte`：允许你输入新的待办事项的文本输入框和按钮。
* `FilterButton.svelte`：允许你对显示的待办事项应用过滤器的 *All*、*Active* 和 *Completed* 按钮。
* `TodosStatus.svelte`：显示“x out of y items completed”（已完成 x 个项目中的 y 个）标题。
* `Todo.svelte`：单个待办事项。每个可见的待办事项将在此组件的独立副本中显示。
* `MoreActions.svelte`：允许你在界面底部对待办事项执行批量操作的 *Check All*（全选）和 *Remove Completed*（删除已完成）按钮。

![我们应用程序中组件的图形表示](/zh-CN/docs/Learn/Tools_and_testing/Client-side_JavaScript_frameworks/Svelte_components/01-todo-components.png)

在本文中，我们将专注于创建 `FilterButton` 和 `Todo` 组件；我们将在以后的文章中介绍其他组件。

让我们开始吧。

**备注：** 在创建我们的前几个组件的过程中，我们还将学习不同的组件间通信技术以及每种技术的优缺点。

提取我们的过滤器组件
----------

我们将首先创建 `FilterButton.svelte` 组件。

1. 首先，创建一个新文件 `components/FilterButton.svelte`。
2. 在这个文件中，我们将声明 `filter` 属性，并从 `Todos.svelte` 中复制相关的标记。将以下内容添加到文件中：

```svelte
<script>
 export let filter = 'all'
</script>

<div class="filters btn-group stack-exception">
  <button class="btn toggle-btn" class:btn\_\_primary={filter === 'all'} aria-pressed={filter === 'all'} on:click={() => filter = 'all'} >
    <span class="visually-hidden">Show</span>
    <span>All</span>
    <span class="visually-hidden">tasks</span>
  </button>
  <button class="btn toggle-btn" class:btn\_\_primary={filter === 'active'} aria-pressed={filter === 'active'} on:click={() => filter = 'active'} >
    <span class="visually-hidden">Show</span>
    <span>Active</span>
    <span class="visually-hidden">tasks</span>
  </button>
  <button class="btn toggle-btn" class:btn\_\_primary={filter === 'completed'} aria-pressed={filter === 'completed'} on:click={() => filter = 'completed'} >
    <span class="visually-hidden">Show</span>
    <span>Completed</span>
    <span class="visually-hidden">tasks</span>
  </button>
</div>
```

3. 回到我们的 `Todos.svelte` 组件，我们想要使用我们的 `FilterButton` 组件。首先，我们需要导入它。在 `Todos.svelte` 的 `<script>` 区块的顶部添加以下行：

```js
import FilterButton from "./FilterButton.svelte";
```

4. 现在，将 `<div class="filters...` 元素替换为对 `FilterButton` 组件的调用，该组件将当前的过滤器作为属性。以下一行就足够了：

```svelte
<FilterButton {filter} />
```

**备注：** 请记住，当 HTML 属性名称和变量匹配时，可以用 `{变量}` 来替换它们。这就是为什么我们可以将 `<FilterButton filter={filter} />` 替换为 `<FilterButton {filter} />`。

到目前为止一切顺利！现在让我们试试该应用程序。你会注意到，当你点击过滤器按钮时，它们会被选中，并且样式会相应更新。但是我们有一个问题：待办事项没有被正确过滤。这是因为 `filter` 变量通过属性从 `Todos` 组件向下流动到 `FilterButton` 组件，但是在 `FilterButton` 组件中发生的更改不会向上流回其父组件——默认情况下，数据绑定是单向的。让我们看看解决这个问题的方法。

在组件之间共享数据：将处理器作为属性传递
--------------------

让子组件通知其父组件发生的任何更改的一种方法是将处理器作为属性传递。子组件将执行处理器，并将所需的信息作为参数传递给父组件的处理器，从而修改父组件的状态。

在我们的例子中，`FilterButton` 组件将从其父组件接收一个 `onclick` 处理器。每当用户点击任何过滤器按钮时，子组件将调用 `onclick` 处理器，并将所选的过滤器作为参数传递回其父组件。

我们只需要声明 `onclick` 属性，并分配一个虚拟处理器以防止错误，就像这样：

```js
export let onclick = (clicked) => {};
```

并且我们将声明一个响应式语句 `$: onclick(filter)`，以在更新 `filter` 变量时调用 `onclick` 处理器。

1. 我们的 `FilterButton` 组件的 `<script>` 部分应该如下所示。现在更新它：

```js
export let filter = "all";
export let onclick = (clicked) => {};
$: onclick(filter);
```

2. 现在，在 `Todos.svelte` 中调用 `FilterButton` 时，我们需要指定处理器。更新如下：

```svelte
<FilterButton {filter} onclick={ (clicked) => filter = clicked }/>
```

当点击任何过滤器按钮时，我们只需要用新的过滤器更新 `filter` 变量。现在我们的 `FilterButton` 组件将再次正常工作。

使用 bind 指令实现更简单的双向数据绑定
----------------------

在前面的示例中，我们意识到我们的 `FilterButton` 组件无法正常工作，是因为我们的应用程序状态通过 `filter` 属性从父组件流向子组件，但是无法返回。因此，我们添加了 `onclick` 属性，用于让子组件将新的 `filter` 值传递给父组件。

这种方法可以正常工作，但是 Svelte 提供了一种更简单、更直接的方法来实现双向数据绑定。通常情况下，数据通过属性从父组件向子组件流动。如果我们希望它也能从子组件向父组件流动，我们可以使用 `bind:` 指令。

通过使用 `bind`，我们告诉 Svelte，在 `FilterButton` 组件中对 `filter` 属性进行的任何更改都应该传播回父组件 `Todos`。也就是说，我们将父组件中的 `filter` 变量的值绑定到子组件中的值。

1. 在 `Todos.svelte` 中，将对 `FilterButton` 组件的调用更新如下：

```svelte
<FilterButton bind:filter={filter} />
```

如往常一样，Svelte 为我们提供了一个简洁的写法：`bind:value={value}` 等同于 `bind:value`。因此，在上面的示例中，你可以简单地写成 `<FilterButton bind:filter />`。
2. 子组件现在可以修改父组件的 filter 变量的值，因此我们不再需要 `onclick` 属性。将 `FilterButton` 的 `<script>` 元素修改为以下内容：

```svelte
<script>
 export let filter = "all";
</script>
```

3. 再次尝试你的应用程序，你应该仍然可以看到你的过滤器正常工作。
创建我们的 Todo 组件
-------------

现在，我们将创建一个 `Todo` 组件，用于封装每个单独的待办事项，包括复选框和一些编辑逻辑，以便你可以更改现有的待办事项。

我们的 `Todo` 组件将接收一个 `todo` 对象作为属性。让我们声明 `todo` 属性并将代码从 `Todos` 组件中移动到其中。暂时，我们将删除对 `removeTodo` 的调用，并替换为一个提示框。稍后我们将重新添加该功能。

1. 创建一个新的组件文件 `components/Todo.svelte`。
2. 将以下内容放入该文件中：

```svelte
<script>
 export let todo
</script>

<div class="stack-small">
  <div class="c-cb">
    <input type="checkbox" id="todo-{todo.id}"
 on:click={() => todo.completed = !todo.completed}
 checked={todo.completed}
 />
    <label for="todo-{todo.id}" class="todo-label">{todo.name}</label>
  </div>
  <div class="btn-group">
    <button type="button" class="btn">
      Edit <span class="visually-hidden">{todo.name}</span>
    </button>
    <button type="button" class="btn btn\_\_danger" on:click={() => alert('not implemented')}>
      Delete <span class="visually-hidden">{todo.name}</span>
    </button>
  </div>
</div>
```

3. 现在，我们需要将 `Todo` 组件导入到 `Todos.svelte` 中。现在转到该文件，并在之前的导入语句下面添加以下 `import` 语句：

```js
import Todo from "./Todo.svelte";
```

4. 接下来，我们需要更新 `{#each}` 块，以包含每个待办事项的 `<Todo>` 组件，而不是移动到 `Todo.svelte` 的代码。我们还将当前的 `todo` 对象作为一个属性传递给组件。
 将 `Todos.svelte` 中的 `{#each}` 块更新如下：

```svelte
<ul role="list" class="todo-list stack-large" aria-labelledby="list-heading">
  {#each filterTodos(filter, todos) as todo (todo.id)}
  <li class="todo">
    <Todo {todo} />
  </li>
  {:else}
  <li>Nothing to do here!</li>
  {/each}
</ul>
```

待办事项列表将显示在页面上，复选框应该可以正常工作（尝试勾选/取消勾选几个，然后观察过滤器是否按预期工作），但是我们的“x out of y items completed”状态标题将不再相应地更新。这是因为我们的 `Todo` 组件通过属性接收待办事项，但没有向其父组件发送任何信息。我们将在以后修复这个问题。
在组件之间共享数据：属性向下传递、事件向上传播模式
-------------------------

`bind` 指令非常简单直观，可以让你在父组件和子组件之间共享数据。然而，当应用程序变得更大、更复杂时，便难以跟踪所有绑定的值。另一种方法是使用“属性向下传递、事件向上传播”通信模式。

基本上，该模式依赖于子组件通过属性从父组件接收数据，而父组件通过处理子组件发出的事件来更新其状态。因此，从父组件向子组件*流动*属性，从子组件向父组件*冒泡*事件。这种模式建立了一种可预测且更易于理解的信息双向流动方式。

让我们看看如何使用自定义事件重新实现缺失的*删除*按钮功能。

为了创建自定义事件，我们将使用 `createEventDispatcher` 程序。它将返回一个 `dispatch()` 函数，允许我们发出自定义事件。当你发出一个事件时，你需要传递事件的名称，以及（可选地）包含要传递给每个监听器的附加信息的对象。这些附加数据将在事件对象的 `detail` 属性中。

**备注：** Svelte 中的自定义事件与常规 DOM 事件共享相同的 API。此外，你可以通过指定 `on:event` 通过将事件向上传递给父组件，而无需指定处理器。

我们将编辑我们的 `Todo` 组件，以发出一个 `remove` 事件，并传递被删除的待办事项作为附加信息。

1. 首先，在 `Todo` 组件的 `<script>` 部分的顶部添加以下行：

```js
import { createEventDispatcher } from "svelte";
const dispatch = createEventDispatcher();
```

2. 现在，在同一文件的标记部分更新*删除*按钮，如下所示：

```svelte
<button type="button" class="btn btn\_\_danger" on:click={() => dispatch('remove', todo)}>
  Delete <span class="visually-hidden">{todo.name}</span>
</button>
```

使用 `dispatch('remove', todo)`，我们发出 `remove` 事件，并将要删除的 `todo` 作为附加数据传递。处理器将被调用，实参为可用的事件对象，附加数据位于 `event.detail` 属性中。
3. 现在我们要从 `Todos.svelte` 内部监听该事件，并根据情况采取行动。返回到该文件，并像下面这样更新对 `<Todo>` 组件的调用：

```svelte
<Todo {todo} on:remove={(e) => removeTodo(e.detail)} />
```

我们的处理程序接收 `e` 参数（事件对象），如前所述，在 `detail` 属性中保存了要删除的待办事项。
4. 现在，如果你再次尝试应用程序，你应该可以看到*删除*功能现在又正常工作了。所以我们的自定义事件已经按照我们的预期工作。此外，`remove` 事件监听器将数据更改发送回父组件，因此我们的“x out of y items completed”状态标题现在在删除待办事项时会适当地更新。

现在我们将处理 `update` 事件，以便父组件可以收到任何已修改的待办事项的通知。

更新待办事项
------

我们仍然需要实现编辑现有的待办事项的功能。我们将在 `Todo` 组件中包含一个编辑模式。进入编辑模式时，将显示一个 `<input>` 字段，允许我们编辑当前的待办事项名称，并提供两个按钮以确认或取消我们的更改。

### 处理事件

1. 我们需要一个变量来跟踪是否处于编辑模式，另一个变量来存储正在更新的任务的名称。在 `Todo` 组件的 `<script>` 部分底部添加以下变量定义：

```js
let editing = false; // 跟踪编辑模式
let name = todo.name; // 存储被编辑的待办事项名称
```

2. 我们需要决定 `Todo` 组件将发出哪些事件：
    * 我们可以为状态切换和名称编辑分别发出不同的事件（例如 `updateTodoStatus` 和 `updateTodoName`）。
    * 或者我们可以采用更通用的方法，为这两个操作发出一个单独的 `update` 事件。我们将采用第二种方法，以便演示一种不同的技术。这种方法的优点是，以后我们可以为待办事项添加更多字段，并且仍然可以使用相同的事件处理所有更新。
 让我们创建 `update()` 函数，该函数将接收更改并发出更新事件，该事件带有修改后的待办事项。再次将以下内容添加到 `<script>` 部分的底部：

```js
function update(updatedTodo) {
  todo = { ...todo, ...updatedTodo }; // 将修改应用于待办事项
  dispatch("update", todo); // 发出更新事件
}
```

在这里，我们使用了展开语法，将修改应用于原始的待办事项。
3. 接下来，我们将创建不同的函数来处理每个用户操作。当待办事项处于编辑模式时，用户可以保存或取消更改。当不在编辑模式时，用户可以删除待办事项、编辑待办事项或在完成和未完成之间切换其状态。
 在之前的函数下面添加以下函数，用于处理这些操作：

```js
function onCancel() {
  name = todo.name; // 将名称恢复为初始值，并
  editing = false; // 退出编辑模式
}

function onSave() {
  update({ name }); // 更新待办事项名称
  editing = false; // 退出编辑模式
}

function onRemove() {
  dispatch("remove", todo); // 发出删除事件
}

function onEdit() {
  editing = true; // 进入编辑模式
}

function onToggle() {
  update({ completed: !todo.completed }); // 更新待办事项状态
}
```

### 更新标记

现在我们需要更新 `Todo` 组件的标记，以在适当的操作发生时调用上述函数。

为了处理编辑模式，我们使用了布尔类型的 `editing` 变量。当它为 `true` 时，它应该显示用于编辑待办事项名称的 `<input>` 字段以及 *Cancel* 和 *Save* 按钮。当它不处于编辑模式时，它将显示复选框、待办事项名称以及编辑和删除待办事项的按钮。

为了实现这一点，我们将使用 `if` 块。`if` 块会根据条件渲染标记。请注意，它不仅仅是根据条件显示或隐藏标记——它会根据条件在 DOM 中动态添加和移除元素。

例如，当 `editing` 为 `true` 时，Svelte 会显示更新表单；当它为 `false` 时，它会将其从 DOM 中移除并添加复选框。由于 Svelte 的响应性，仅仅分配 `editing` 变量的值就足以显示正确的 HTML 元素。

以下是基本的 `if` 块结构示例：

```svelte
<div class="stack-small">
  {#if editing}
  <!-- 用于编辑待办事项的标记：标签、输入文本、取消和保存按钮 -->
  {:else}
  <!-- 用于显示待办事项的标记：复选框、标签、编辑和删除按钮 -->
  {/if}
</div>
```

非编辑部分（即 `if` 块的 `{:else}` 部分（下半部分））与我们在 `Todos` 组件中的部分非常相似。唯一的区别在于，根据用户的操作，我们调用 `onToggle()`、`onEdit()` 和 `onRemove()`。

```svelte
{:else}
  <div class="c-cb">
    <input type="checkbox" id="todo-{todo.id}"
 on:click={onToggle} checked={todo.completed}
 >
    <label for="todo-{todo.id}" class="todo-label">{todo.name}</label>
  </div>
  <div class="btn-group">
    <button type="button" class="btn" on:click={onEdit}>
      Edit<span class="visually-hidden"> {todo.name}</span>
    </button>
    <button type="button" class="btn btn\_\_danger" on:click={onRemove}>
      Delete<span class="visually-hidden"> {todo.name}</span>
    </button>
  </div>
{/if}
</div>
```

值得注意的是：

* 当用户点击*编辑*按钮时，我们执行 `onEdit()` 函数，该函数将 `editing` 变量设置为 `true`。
* 当用户点击复选框时，我们调用 `onToggle()` 函数，该函数执行 `update()`，并传递一个包含新 `completed` 值的对象作为参数。
* `update()` 函数会发出 `update` 事件，并传递原始待办事项的副本，以及应用的更改。
* 最后，`onRemove()` 函数会发出 `remove` 事件，并将待删除的 `todo` 作为附加数据传递。

编辑界面（上半部分）将包含 `<input>` 字段和两个用于取消或保存更改的按钮：

```svelte
<div class="stack-small">
{#if editing}
  <form on:submit|preventDefault={onSave} class="stack-small" on:keydown={(e) => e.key === 'Escape' && onCancel()}>
    <div class="form-group">
      <label for="todo-{todo.id}" class="todo-label">New name for '{todo.name}'</label>
      <input bind:value={name} type="text" id="todo-{todo.id}" autoComplete="off" class="todo-text" />
    </div>
    <div class="btn-group">
      <button class="btn todo-cancel" on:click={onCancel} type="button">
        Cancel<span class="visually-hidden">renaming {todo.name}</span>
        </button>
      <button class="btn btn\_\_primary todo-edit" type="submit" disabled={!name}>
        Save<span class="visually-hidden">new name for {todo.name}</span>
      </button>
    </div>
  </form>
{:else}
[...]
```

当用户点击*编辑*按钮时，`editing` 变量将被设置为 `true`，然后 Svelte 会从 DOM 中删除 `{:else}` 部分的标记，并将其替换为 `{#if}` 部分的标记。

`<input>` 的 `value` 属性将绑定到 `name` 变量，取消和保存更改的按钮分别调用 `onCancel()` 和 `onSave()`（我们之前添加了这些函数）：

* 当调用 `onCancel()` 时，`name` 将恢复为其原始值（当作为属性传递时），并且我们退出编辑模式（通过将 `editing` 设置为 `false`）。
* 当调用 `onSave()` 时，我们运行 `update()` 函数，将修改后的 `name` 传递给它，并退出编辑模式。

我们还借助 `disabled={!name}` 属性以在 `<input>` 为空时禁用*保存*按钮，并允许用户使用 `Escape` 键取消编辑，如下所示：
```

on:keydown={(e) => e.key === 'Escape' && onCancel()}
```

我们还使用 `todo.id` 为新的输入控件和标签创建唯一的 id。

1. 我们的 `Todo` 组件的完整更新标记如下。现在请更新你的标记：

```svelte
<div class="stack-small">
{#if editing}
  <!-- 用于编辑待办事项的标记：标签、输入文本、取消和保存按钮 -->
  <form on:submit|preventDefault={onSave} class="stack-small" on:keydown={(e) => e.key === 'Escape' && onCancel()}>
    <div class="form-group">
      <label for="todo-{todo.id}" class="todo-label">New name for '{todo.name}'</label>
      <input bind:value={name} type="text" id="todo-{todo.id}" autoComplete="off" class="todo-text" />
    </div>
    <div class="btn-group">
      <button class="btn todo-cancel" on:click={onCancel} type="button">
        Cancel<span class="visually-hidden">renaming {todo.name}</span>
        </button>
      <button class="btn btn\_\_primary todo-edit" type="submit" disabled={!name}>
        Save<span class="visually-hidden">new name for {todo.name}</span>
      </button>
    </div>
  </form>
{:else}
  <!-- 用于显示待办事项的标记：复选框、标签、编辑和删除按钮 -->
  <div class="c-cb">
    <input type="checkbox" id="todo-{todo.id}"
 on:click={onToggle} checked={todo.completed}
 >
    <label for="todo-{todo.id}" class="todo-label">{todo.name}</label>
  </div>
  <div class="btn-group">
    <button type="button" class="btn" on:click={onEdit}>
      Edit<span class="visually-hidden"> {todo.name}</span>
    </button>
    <button type="button" class="btn btn\_\_danger" on:click={onRemove}>
      Delete<span class="visually-hidden"> {todo.name}</span>
    </button>
  </div>
{/if}
</div>
```

**备注：** 我们可以进一步将其拆分为两个不同的组件，一个用于编辑待办事项，另一个用于显示待办事项。最终，这取决于你是否对在单个组件中处理这种复杂性感到舒适。你还应考虑进一步拆分的话，是否能够在其他的上下文中重用此组件。
2. 要使更新功能起作用，我们必须在 `Todos` 组件 中处理 `update` 事件。在 `Todos` 组件的 `<script>` 部分中添加以下处理器：

```js
function updateTodo(todo) {
  const i = todos.findIndex((t) => t.id === todo.id);
  todos[i] = { ...todos[i], ...todo };
}
```

我们通过 `id` 在 `todos` 数组中找到相应的 `todo`，并使用展开语法更新其内容。在这种情况下，我们也可以只使用 `todos[i] = todo`，但这种实现更加健壮，允许 `Todo` 组件仅返回待办事项的更新部分。
3. 接下来，我们需要在 `<Todo>` 组件调用中监听 `update` 事件，并在发生此事件时运行 `updateTodo()` 函数来更改 `name` 和 `completed` 状态。更新你的 <Todo> 调用如下：

```svelte
{#each filterTodos(filter, todos) as todo (todo.id)}
<li class="todo">
  <Todo {todo} on:update={(e) => updateTodo(e.detail)} on:remove={(e) =>
 removeTodo(e.detail)} />
</li>
```

4. 再次尝试你的应用程序，你将看到你可以删除、添加、编辑、取消编辑和切换待办事项的完成状态。当待办事项完成时，“x out of y items completed”的状态标题将相应更新。

正如你所见，在 Svelte 中实现“属性向下传递、事件向上传播”模式很容易。尽管如此，对于简单的组件，`bind` 可能是一个不错的选择；Svelte 允许你自由选择。

**备注：** Svelte 提供了更高级的机制来在组件之间共享信息：Context API 和 Store。Context API 提供了一种机制，使组件及其后代组件能够在不传递数据和函数作为属性或分发大量事件的情况下进行通信。Store 允许你在不具有层次关系的组件之间共享响应式数据。我们将在本系列的后续内容中介绍 Store。

到目前为止的代码
--------

### 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 中查看代码的当前状态，请访问：

https://svelte.dev/repl/76cc90c43a37452e8c7f70521f88b698?version=3.23.2

总结
--

现在，我们已经完成了应用程序所需的所有功能。我们可以显示、添加、编辑和删除待办事项，将它们标记为已完成，并按状态进行筛选。

在本文中，我们涵盖了以下主题：

* 将功能提取到新组件中
* 使用作为属性接收到的处理程序将信息从子组件传递到父组件
* 使用 `bind` 指令将信息从子组件传递到父组件
* 使用 `if` 块有条件地渲染标记块
* 实现“属性向下传递、事件向上传播”通信模式
* 创建和监听自定义事件

在下一篇文章中，我们将继续将我们的应用程序组件化，并了解一些在处理 DOM 方面的高级技巧。

* 上一页
* Overview: Client-side JavaScript frameworks
* 下一页