Skip to content

组件介绍

组件允许我们将 UI 划分为独立的、可重用的部分,并且可以对每个部分进行单独的思考。在实际应用中,组件常常被组织成层层嵌套的树状结构:

组件树

这和我们嵌套 HTML 元素的方式类似,Vue 实现了自己的组件模型,使我们可以在每个组件内封装自定义内容与逻辑。

定义一个组件

当使用构建步骤时,我们一般会将 Vue 组件定义在一个单独的 .vue 文件中,这被叫做单文件组件 (简称 SFC):

vue
<script setup>
import { ref } from "vue";

const count = ref(0);
</script>

<template>
  <button @click="count++">You clicked me {{ count }} times.</button>
</template>

这里的模板是一个内联的 JavaScript 字符串,Vue 将会在运行时编译它。你也可以使用 ID 选择器来指向一个元素 (通常是原生的 <template> 元素),Vue 将会使用其内容作为模板来源。

上面的例子中定义了一个组件,并在一个 .js 文件里默认导出了它自己,但你也可以通过具名导出在一个文件中导出多个组件。

使用组件

我们会在接下来的指引中使用 SFC 语法,无论你是否使用构建步骤,组件相关的概念都是相同的。示例一节中展示了两种场景中的组件使用情况。

要使用一个子组件,我们需要在父组件中导入它。假设我们把计数器组件放在了一个叫做 ButtonCounter.vue 的文件中,这个组件将会以默认导出的形式被暴露给外部。

vue
<script setup>
import ButtonCounter from "./ButtonCounter.vue";
</script>

<template>
  <h1>Here is a child component!</h1>
  <ButtonCounter />
</template>

通过 <script setup>,导入的组件都在模板中直接可用。

当然,你也可以全局地注册一个组件,使得它在当前应用中的任何组件上都可以使用,而不需要额外再导入。关于组件的全局注册和局部注册两种方式的利弊,我们放在了组件注册这一章节中专门讨论。

组件可以被重用任意多次:

vue
<h1>Here is a child component!</h1>
<ButtonCounter />
<ButtonCounter />
<ButtonCounter />

你会注意到,每当点击这些按钮时,每一个组件都维护着自己的状态,是不同的 count。这是因为每当你使用一个组件,就创建了一个新的* *实例**。

在单文件组件中,推荐为子组件使用 PascalCase 的标签名,以此来和原生的 HTML 元素作区分。虽然原生 HTML 标签名是不区分大小写的,但 Vue 单文件组件是可以在编译中区分大小写的。我们也可以使用 /> 来关闭一个标签。

如果你是直接在 DOM 中书写模板 (例如原生 <template> 元素的内容),模板的编译需要遵从浏览器中 HTML 的解析行为。在这种情况下,你应该需要使用 kebab-case 形式并显式地关闭这些组件的标签。

template

vue
<!-- 如果是在 DOM 中书写该模板 -->
<button-counter></button-counter>
<button-counter></button-counter>
<button-counter></button-counter>

案例

不使用组件

制作一个折叠面板

image-20210115092834016

需求:现在想要多个收起展开的部分

方案 1: 复制代码

  • 代码重复冗余
  • 不利于维护

模板标签 - 在这个基础上,把要复用的多复制几份 (讲解不好的地方引出解决方案)

vue
<script setup>
import { ref } from "vue";

const is_show = ref(false);
</script>
vue
<template>
  <div class="container">
    <h3>案例:折叠面板</h3>
    <div>
      <div class="title">
        <h4>芙蓉楼送辛渐</h4>
        <span class="btn" @click="is_show = !is_show">
          {{ isShow ? "收起" : "展开" }}
        </span>
      </div>
      <div class="container" v-show="is_show">
        <p>寒雨连江夜入吴,</p>
        <p>平明送客楚山孤。</p>
        <p>洛阳亲友如相问,</p>
        <p>一片冰心在玉壶。</p>
      </div>
    </div>
  </div>
</template>
vue
<style>
body {
  background-color: #ccc;
}

.container {
  width: 400px;
  margin: 20px auto;
  background-color: #fff;
  border: 4px solid blueviolet;
  border-radius: 1em;
  box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.5);
  padding: 1em 2em 2em;
}

h3 {
  text-align: center;
}

.title {
  display: flex;
  justify-content: space-between;
  align-items: center;
  border: 1px solid #ccc;
  padding: 0 1em;
}

.title h4 {
  line-height: 2;
  margin: 0;
}

.container {
  border: 1px solid #ccc;
  padding: 0 1em;
}

.btn {
  /* 鼠标改成手的形状 */
  cursor: pointer;
}
</style>

上面代码复制 3 份,发生变化一起变化

解决方案:不同的部分,用不同的 isShow 变量

vue
<script setup>
import { ref } from "vue";

const is_show = ref(false);
const is_show1 = ref(false);
const is_show2 = ref(false);
</script>

<template>
  <div class="container">
    <h3>案例:折叠面板</h3>
    <div>
      <div class="title">
        <h4>芙蓉楼送辛渐</h4>
        <span class="btn" @click="is_show = !is_show">
          {{ is_show ? "收起" : "展开" }}
        </span>
      </div>
      <div class="container" v-show="is_show">
        <p>寒雨连江夜入吴,</p>
        <p>平明送客楚山孤。</p>
        <p>洛阳亲友如相问,</p>
        <p>一片冰心在玉壶。</p>
      </div>
    </div>
    <div>
      <div class="title">
        <h4>芙蓉楼送辛渐</h4>
        <span class="btn" @click="is_show1 = !is_show1">
          {{ is_show1 ? "收起" : "展开" }}
        </span>
      </div>
      <div class="container" v-show="is_show1">
        <p>寒雨连江夜入吴,</p>
        <p>平明送客楚山孤。</p>
        <p>洛阳亲友如相问,</p>
        <p>一片冰心在玉壶。</p>
      </div>
    </div>
    <div>
      <div class="title">
        <h4>芙蓉楼送辛渐</h4>
        <span class="btn" @click="is_show2 = !is_show2">
          {{ is_show2 ? "收起" : "展开" }}
        </span>
      </div>
      <div class="container" v-show="is_show2">
        <p>寒雨连江夜入吴,</p>
        <p>平明送客楚山孤。</p>
        <p>洛阳亲友如相问,</p>
        <p>一片冰心在玉壶。</p>
      </div>
    </div>
  </div>
</template>

总结:代码非常的冗余和重复吧?解决方案呢?就是采用我们的组件化开发的方式,往下看

使用组件

组件是可复用的 Vue 实例,封装标签,样式和 JS 代码

组件化 :封装的思想,把页面上 可重用的部分 封装为 组件,从而方便项目的 开发 和 维护

一个页面,可以拆分成一个个组件,一个组件就是一个整体,每个组件可以有自己独立的 结构 样式 和 行为 (html, css 和 js)

image-20210216114452712

@ 基础使用

目标:每个组件都是一个独立的个体,代码里体现为一个独立的.vue 文件

口诀:哪部分标签复用,就把哪部分封装到组件内

步骤:

  1. 创建组件 components/Panel.vue

    封装 html + css + js - 组件都是独立的,为了复用

vue
<script setup>
import { ref } from "vue";

const is_show = ref(false);
</script>
vue
<template>
  <div>
    <div class="title">
      <h4>芙蓉楼送辛渐</h4>
      <span class="btn" @click="is_show = !is_show">
        {{ is_show ? "收起" : "展开" }}
      </span>
    </div>
    <div class="container" v-show="is_show">
      <p>寒雨连江夜入吴,</p>
      <p>平明送客楚山孤。</p>
      <p>洛阳亲友如相问,</p>
      <p>一片冰心在玉壶。</p>
    </div>
  </div>
</template>
vue
<style scoped>
.title {
  display: flex;
  justify-content: space-between;
  align-items: center;
  border: 1px solid #ccc;
  padding: 0 1em;
}

.title h4 {
  line-height: 2;
  margin: 0;
}

.container {
  border: 1px solid #ccc;
  padding: 0 1em;
}

.btn {
  /* 鼠标改成手的形状 */
  cursor: pointer;
}
</style>

scoped 作用

目的:解决多个组件样式名相同,冲突问题

需求:div 标签名选择器,设置背景色

问题:发现组件里的 div 和外面的 div 都生效了

解决:给 Panel.vue 组件里 style 标签上加 scoped 属性即可

vue
<style scoped></style>

在 style 上加入 scoped 属性,就会在此组件的标签上加上一个随机生成的 data-v 开头的属性

而且必须是当前组件的元素,才会有这个自定义属性,才会被这个样式作用到

image-20210216122749906

总结:style 上加 scoped, 组件内的样式只在当前 vue 组件生效