创建我们的第一个 Vue 组件

现在是深入了解 Vue 并创建我们自己的自定义组件的时候了——我们将从创建一个组件来表示待办事项列表中的每个项目开始。在此过程中,我们将学习一些重要的概念,例如在其他组件内部调用组件、通过 props 将数据传递给它们以及保存数据状态。

注意:如果您需要将您的代码与我们的版本进行对比,您可以在我们的 todo-vue 代码库 中找到示例 Vue 应用代码的完成版本。要查看运行的实时版本,请访问 https://mdn.github.io/todo-vue/

先决条件

熟悉核心 HTMLCSSJavaScript 语言,了解 终端/命令行

Vue 组件编写为 JavaScript 对象(管理应用程序的数据)和基于 HTML 的模板语法(映射到底层 DOM 结构)的组合。对于安装以及使用 Vue 的一些更高级功能(如单文件组件或渲染函数),您将需要一个安装了 Nodenpm 的终端。

目标 学习如何创建 Vue 组件、在另一个组件中渲染它、使用 props 将数据传递给它以及保存其状态。

创建 ToDoItem 组件

让我们创建我们的第一个组件,它将显示单个待办事项。我们将使用它来构建我们的待办事项列表。

  1. 在您的 moz-todo-vue/src/components 目录中,创建一个名为 ToDoItem.vue 的新文件。在您的代码编辑器中打开该文件。
  2. 通过在文件顶部添加 <template></template> 来创建组件的模板部分。
  3. 在模板部分下方创建一个 <script></script> 部分。在 <script> 标签内,添加一个默认导出的对象 export default {},它是您的组件对象。

您的文件现在应该如下所示

标记
<template></template>
<script>
export default {};
</script>

我们现在可以开始向我们的 ToDoItem 添加实际内容了。Vue 模板目前只允许一个根元素——一个元素需要包装模板部分内的所有内容(这将在 Vue 3 发布时发生变化)。我们将使用 <div> 作为该根元素。

  1. 现在在您的组件模板中添加一个空的 <div>
  2. 在该 <div> 内,让我们添加一个复选框和一个相应的标签。向复选框添加一个 id,并添加一个 for 属性,将复选框映射到标签,如下所示。
    标记
    <template>
      <div>
        <input type="checkbox" id="todo-item" />
        <label for="todo-item">My Todo Item</label>
      </div>
    </template>
    

在我们的应用程序中使用 TodoItem

这一切都很好,但我们还没有将组件添加到我们的应用程序中,因此无法测试它并查看一切是否正常。让我们现在添加它。

  1. 再次打开 App.vue
  2. 在您的 <script> 标签的顶部,添加以下内容以导入您的 ToDoItem 组件
    js
    import ToDoItem from "./components/ToDoItem.vue";
    
  3. 在您的组件对象中,添加 components 属性,并在其中添加您的 ToDoItem 组件以注册它。

您的 <script> 内容现在应该如下所示

js
import ToDoItem from "./components/ToDoItem.vue";

export default {
  name: "app",
  components: {
    ToDoItem,
  },
};

这与 Vue CLI 早期注册 HelloWorld 组件的方式相同。

要实际在应用程序中渲染 ToDoItem 组件,您需要向上进入您的 <template> 元素并将其作为 <to-do-item></to-do-item> 元素调用。请注意,组件文件名及其在 JavaScript 中的表示形式为 PascalCase(例如 ToDoList),而等效的自定义元素为 kebab-case(例如 <to-do-list>)。如果您正在 直接在 DOM 中 编写 Vue 模板,则必须使用此大小写样式。

  1. <h1> 下方,创建一个无序列表(<ul>),其中包含一个列表项(<li>)。
  2. 在列表项内添加 <to-do-item></to-do-item>

您的 App.vue 文件的 <template> 部分现在应该如下所示

标记
<div id="app">
  <h1>To-Do List</h1>
  <ul>
    <li>
      <to-do-item></to-do-item>
    </li>
  </ul>
</div>

如果您再次检查渲染的应用程序,您现在应该会看到渲染的 ToDoItem,它包含一个复选框和一个标签。

The current rendering state of the app, which includes a title of To-Do List, and a single checkbox and label

使用 props 使组件动态化

我们的 ToDoItem 组件仍然不是很有用,因为我们只能在一个页面上包含它一次(ID 必须唯一),并且我们无法设置标签文本。这方面没有任何动态性。

我们需要的是一些组件状态。这可以通过向我们的组件添加 props 来实现。您可以将 props 视为类似于函数中的输入。prop 的值会为组件提供影响其显示的初始状态。

注册 props

在 Vue 中,有两种方法可以注册 props

  • 第一种方法是将 props 作为字符串数组列出。数组中的每个条目对应于 prop 的名称。
  • 第二种方法是将 props 定义为对象,每个键对应于 prop 名称。将 props 列出为对象允许您指定默认值、将 props 标记为必需、执行基本对象类型(特别是围绕 JavaScript 原语类型)以及执行简单的 prop 验证。

注意:Prop 验证仅在开发模式下发生,因此您不能严格依赖于生产环境中的验证。此外,prop 验证函数在组件实例创建之前调用,因此它们无法访问组件状态(或其他 props)。

对于此组件,我们将使用对象注册方法。

  1. 返回您的 ToDoItem.vue 文件。
  2. 在导出 default {} 对象内添加一个 props 属性,其中包含一个空对象。
  3. 在此对象内,添加两个键为 labeldone 的属性。
  4. label 键的值应为具有 2 个属性(或在组件上下文中称为 **props**)的对象。
    1. 第一个是 required 属性,其值为 true。这将告诉 Vue 我们期望此组件的每个实例都具有 label 字段。如果 ToDoItem 组件没有 label 字段,Vue 将会警告我们。
    2. 我们将添加的第二个属性是 type 属性。将此属性的值设置为 JavaScript String 类型(注意大写“S”)。这告诉 Vue 我们期望此属性的值为字符串。
  5. 现在转到 done prop。
    1. 首先添加一个 default 字段,其值为 false。这意味着当没有 done prop 传递给 ToDoItem 组件时,done prop 的值将为 false(请记住,这不是必需的——我们只需要在非必需 prop 上使用 default)。
    2. 接下来添加一个 type 字段,其值为 Boolean。这告诉 Vue 我们期望值 prop 为 JavaScript 布尔类型。

您的组件对象现在应该如下所示

js
export default {
  props: {
    label: { required: true, type: String },
    done: { default: false, type: Boolean },
  },
};

使用注册的 props

在组件对象中定义了这些 props 后,我们现在可以在我们的模板中使用这些变量值。让我们首先将 label prop 添加到组件模板中。

在您的 <template> 中,将 <label> 元素的内容替换为 {{label}}

{{}} 是 Vue 中的一种特殊模板语法,它允许我们在模板中打印在我们的类中定义的 JavaScript 表达式的结果,包括值和方法。重要的是要知道 {{}} 内的内容显示为文本而不是 HTML。在本例中,我们正在打印 label prop 的值。

您的组件的模板部分现在应该如下所示

标记
<template>
  <div>
    <input type="checkbox" id="todo-item" />
    <label for="todo-item">{{ label }}</label>
  </div>
</template>

返回您的浏览器,您将看到待办事项像以前一样渲染,但没有标签(哦,不!)。转到浏览器的 DevTools,您将在控制台中看到类似以下内容的警告

[Vue warn]: Missing required prop: "label"

found in

---> <ToDoItem> at src/components/ToDoItem.vue
        <App> at src/App.vue
          <Root>

这是因为我们将 label 标记为必需 prop,但我们从未向组件提供该 prop——我们已在模板中定义了我们希望使用它的位置,但我们在调用它时没有将其传递给组件。让我们解决这个问题。

在您的 App.vue 文件中,将 label prop 添加到 <to-do-item></to-do-item> 组件中,就像常规 HTML 属性一样

标记
<to-do-item label="My ToDo Item"></to-do-item>

现在您将在应用程序中看到标签,并且警告不会再次出现在控制台中。

所以这就是 props 的核心内容。接下来,我们将继续讨论 Vue 如何持久化数据状态。

Vue 的数据对象

如果您更改传递给 App 组件中的 <to-do-item></to-do-item> 调用的 label prop 的值,您应该会看到它更新。这很棒。我们有一个复选框,带有一个可更新的标签。但是,我们目前没有对“done”prop 做任何操作——我们可以在 UI 中选中复选框,但在应用程序的任何地方我们都没有记录待办事项是否真正完成。

为了实现这一点,我们希望将组件的 done prop 绑定到 <input> 元素上的 checked 属性,以便它可以作为复选框是否选中的记录。但是,重要的是 props 充当单向数据绑定——组件绝不应该更改其自身 props 的值。有很多原因导致这种情况。部分原因是组件编辑 props 会使调试成为一项挑战。如果一个值传递给多个子元素,则可能难以跟踪该值的更改来自何处。此外,更改 props 会导致组件重新渲染。因此,在组件中更改 props 将触发组件重新渲染,这反过来可能会再次触发更改。

为了解决此问题,我们可以使用 Vue 的 data 属性管理 done 状态。data 属性是您可以在组件中管理本地状态的地方,它与 props 属性一起位于组件对象内,并具有以下结构

js
data() {
  return {
    key: value
  }
}

您会注意到 data 属性是一个函数。这是为了在运行时为组件的每个实例保持数据值唯一——该函数为每个组件实例分别调用。如果您将数据声明为只是一个对象,则该组件的所有实例将共享相同的值。这是 Vue 注册组件的方式产生的副作用,并且是您不希望发生的事情。

正如您所料,您可以使用 this 从数据内部访问组件的 props 和其他属性。我们很快就会看到一个示例。

注意:由于箭头函数中 this 的工作方式(绑定到父级的上下文),因此如果您使用箭头函数,则将无法从 data 内部访问任何必要的属性。因此,请不要对 data 属性使用箭头函数。

因此,让我们向我们的 ToDoItem 组件添加一个 data 属性。这将返回一个包含单个属性的对象,我们将该属性称为 isDone,其值为 this.done

像这样更新组件对象

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

Vue 在这里做了一些魔法——它将所有 props 直接绑定到组件实例,因此我们不必调用 this.props.done。它还将其他属性(data,您已经见过,以及其他属性,如 methodscomputed 等)直接绑定到实例。部分原因是为了使它们对您的模板可用。这样做的缺点是您需要使这些属性的键保持唯一。这就是为什么我们将我们的 data 属性称为 isDone 而不是 done 的原因。

因此,现在我们需要将 isDone 属性附加到我们的组件。与 Vue 使用 {{}} 表达式在模板中显示 JavaScript 表达式的方式类似,Vue 有一种特殊的语法将 JavaScript 表达式绑定到 HTML 元素和组件:v-bindv-bind 表达式如下所示

v-bind:attribute="expression"

换句话说,您需要在想要绑定的任何属性/prop 前加上v-bind:。在大多数情况下,您可以使用v-bind属性的简写形式,即在属性/prop 前加上冒号。因此,:attribute="expression"v-bind:attribute="expression"的效果相同。

因此,在我们ToDoItem组件中的复选框的情况下,我们可以使用v-bindisDone属性映射到<input>元素上的checked属性。以下两种写法是等价的

标记
<input type="checkbox" id="todo-item" v-bind:checked="isDone" />

<input type="checkbox" id="todo-item" :checked="isDone" />

您可以随意使用任何一种模式。不过最好保持一致。由于简写语法更常用,本教程将坚持使用这种模式。

让我们来做吧。现在更新您的<input>元素以包含:checked="isDone"

通过将:done="true"传递给App.vue中的ToDoItem调用来测试您的组件。请注意,您需要使用v-bind语法,否则true将作为字符串传递。显示的复选框应被选中。

标记
<template>
  <div id="app">
    <h1>My To-Do List</h1>
    <ul>
      <li>
        <to-do-item label="My ToDo Item" :done="true"></to-do-item>
      </li>
    </ul>
  </div>
</template>

尝试将true更改为false,然后再改回,并在两者之间重新加载应用程序,以查看状态如何变化。

为 Todos 提供唯一 ID

太棒了!我们现在有一个可以以编程方式设置状态的工作复选框。但是,我们目前只能在页面上添加一个ToDoList组件,因为id是硬编码的。这会导致辅助技术的错误,因为id需要正确地将标签映射到它们的复选框。为了解决这个问题,我们可以在组件数据中以编程方式设置id

我们可以使用nanoid包来帮助保持索引唯一。此包导出一个函数nanoid(),该函数生成一个唯一的字符串。这足以保持组件id的唯一性。

让我们使用npm将包添加到我们的项目中;停止您的服务器并在终端中输入以下命令

bash
npm install --save nanoid

注意:如果您更喜欢yarn,则可以使用yarn add nanoid

现在我们可以将此包导入到我们的ToDoItem组件中。在ToDoItem.vue<script>元素顶部添加以下行

js
import { nanoid } from "nanoid";

接下来,在我们的数据属性中添加一个id字段,以便组件对象最终看起来像这样(nanoid()返回一个具有指定前缀的唯一字符串——todo-

js
import { nanoid } from "nanoid";

export default {
  props: {
    label: { required: true, type: String },
    done: { default: false, type: Boolean },
  },
  data() {
    return {
      isDone: this.done,
      id: "todo-" + nanoid(),
    };
  },
};

接下来,将id绑定到复选框的id属性和标签的for属性,更新现有的idfor属性,如下所示

标记
<template>
  <div>
    <input type="checkbox" :id="id" :checked="isDone" />
    <label :for="id">{{ label }}</label>
  </div>
</template>

总结

本文到此结束。此时,我们有一个运行良好的ToDoItem组件,可以传递一个要显示的标签,将存储其选中状态,并且每次调用时都会使用唯一的id进行渲染。您可以通过暂时在App.vue中添加更多<to-do-item></to-do-item>调用,然后使用浏览器的开发者工具检查其渲染输出,来检查唯一的id是否有效。

现在,我们准备向我们的应用程序添加多个ToDoItem组件。在下一篇文章中,我们将介绍如何向我们的App.vue组件添加一组待办事项数据,然后我们将循环遍历这些数据并在ToDoItem组件中使用v-for指令显示它们。