跳转到内容

组件基础

组件允许我们将UI拆分为独立和可重用的部分,并单独考虑每个部分。一个应用通常组织成嵌套组件的树

Component Tree

这与我们嵌套原生的HTML元素非常相似,但Vue实现了自己的组件模型,允许我们在每个组件中封装自定义内容和逻辑。Vue也与原生的Web组件很好地配合。如果您对Vue组件和原生Web组件之间的关系感兴趣,请在此了解更多

定义组件

当使用构建步骤时,我们通常使用.vue扩展名定义每个Vue组件在专用文件中,称为单文件组件(SFC)

vue
<script>
export default {
  data() {
    return {
      count: 0
    }
  }
}
</script>

<template>
  <button @click="count++">You clicked me {{ count }} times.</button>
</template>
vue
<script setup>
import { ref } from 'vue'

const count = ref(0)
</script>

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

当不使用构建步骤时,Vue组件可以定义为一个包含Vue特定选项的普通JavaScript对象

js
export default {
  data() {
    return {
      count: 0
    }
  },
  template: `
    <button @click="count++">
      You clicked me {{ count }} times.
    </button>`
}
js
import { ref } from 'vue'

export default {
  setup() {
    const count = ref(0)
    return { count }
  },
  template: `
    <button @click="count++">
      You clicked me {{ count }} times.
    </button>`
  // Can also target an in-DOM template:
  // template: '#my-template-element'
}

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

上述示例定义了一个单个组件,并将其作为 .js 文件的默认导出导出,但您可以使用命名导出从同一文件中导出多个组件。

使用组件

提示

本指南的其余部分将使用 SFC 语法 - 无论是否使用构建步骤,组件的概念都是相同的。《示例》部分显示了两种情况下的组件使用。

要使用子组件,我们需要在父组件中导入它。假设我们将计数器组件放在名为 ButtonCounter.vue 的文件中,该组件将作为文件的默认导出

vue
<script>
import ButtonCounter from './ButtonCounter.vue'

export default {
  components: {
    ButtonCounter
  }
}
</script>

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

为了将导入的组件暴露给我们的模板,我们需要使用 components 选项将其注册。然后,组件将可以使用其注册的键作为标签使用。

vue
<script setup>
import ButtonCounter from './ButtonCounter.vue'
</script>

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

使用 <script setup>,导入的组件将自动对模板可用。

还可以全局注册组件,使其在给定应用程序的所有组件中可用,而无需导入。全局注册与本地注册的优缺点在专门的《组件注册》部分中讨论。

组件可以按需重复使用

模板
<h1>Here are many child components!</h1>
<ButtonCounter />
<ButtonCounter />
<ButtonCounter />

请注意,当点击按钮时,每个按钮都保持自己的独立 count。这是因为每次使用组件时,都会创建一个新的 实例

在 SFC 中,建议使用 PascalCase 标签名来命名子组件,以便与原生 HTML 元素区分开来。虽然原生 HTML 标签名不区分大小写,但 Vue SFC 是一个编译格式,因此我们能够在其中使用大小写敏感的标签名。我们还可以使用 /> 来关闭标签。

如果您直接在 DOM 中编写模板(例如,作为原生 <template> 元素的内容),则模板将受浏览器原生 HTML 解析行为的影响。在这种情况下,您需要使用 kebab-case 和显式关闭标签来为组件

模板
<!-- if this template is written in the DOM -->
<button-counter></button-counter>
<button-counter></button-counter>
<button-counter></button-counter>

有关更多详细信息,请参阅 in-DOM 模板解析注意事项

传递 Props

如果我们正在构建一个博客,我们可能需要一个表示博客文章的组件。我们希望所有博客文章都具有相同的视觉布局,但内容不同。这样的组件除非您可以向它传递数据(例如,我们想要显示的特定文章的标题和内容),否则将没有用处。这正是 props 的作用。

Props 是您可以注册在组件上的自定义属性。要向我们的博客文章组件传递标题,我们必须在组件接受的 props 列表中声明它,使用 props 选项defineProps

vue
<!-- BlogPost.vue -->
<script>
export default {
  props: ['title']
}
</script>

<template>
  <h4>{{ title }}</h4>
</template>

当值传递给 prop 属性时,它成为该组件实例的一个属性。该属性的值在模板和组件的 this 上下文中都是可访问的,就像任何其他组件属性一样。

vue
<!-- BlogPost.vue -->
<script setup>
defineProps(['title'])
</script>

<template>
  <h4>{{ title }}</h4>
</template>

defineProps 是一个编译时宏,仅在 <script setup> 内部可用,无需显式导入。声明的 props 将自动暴露给模板。defineProps 还返回一个包含传递给组件的所有 props 的对象,以便我们可以在需要时在 JavaScript 中访问它们

js
const props = defineProps(['title'])
console.log(props.title)

另请参阅:为组件 Props 类型化

如果您不使用 <script setup>,则应使用 props 选项来声明属性,并且属性对象将被传递给 setup() 作为第一个参数

js
export default {
  props: ['title'],
  setup(props) {
    console.log(props.title)
  }
}

组件可以有任意多的属性,并且默认情况下,任何值都可以传递给任何属性。

一旦注册了属性,您就可以像这样将其作为自定义属性传递数据

模板
<BlogPost title="My journey with Vue" />
<BlogPost title="Blogging with Vue" />
<BlogPost title="Why Vue is so fun" />

然而,在典型的应用程序中,您可能在父组件中有一个帖子数组

js
export default {
  // ...
  data() {
    return {
      posts: [
        { id: 1, title: 'My journey with Vue' },
        { id: 2, title: 'Blogging with Vue' },
        { id: 3, title: 'Why Vue is so fun' }
      ]
    }
  }
}
js
const posts = ref([
  { id: 1, title: 'My journey with Vue' },
  { id: 2, title: 'Blogging with Vue' },
  { id: 3, title: 'Why Vue is so fun' }
])

然后想要使用 v-for 为每个帖子渲染一个组件

模板
<BlogPost
  v-for="post in posts"
  :key="post.id"
  :title="post.title"
 />

请注意,如何使用 v-bind 语法:title="post.title")来传递动态属性值。这在您不知道将要渲染的确切内容时非常有用。

这就是您现在需要了解的所有关于属性的内容,但是一旦您完成阅读此页面并熟悉其内容,我们建议稍后返回阅读关于 属性 的完整指南。

监听事件

随着我们开发 <BlogPost> 组件,一些功能可能需要向上与父组件通信。例如,我们可能决定包含一个辅助功能来放大博客文章的文本,同时保持页面其余部分默认大小。

在父组件中,我们可以通过添加一个 postFontSize 数据属性ref

js
data() {
  return {
    posts: [
      /* ... */
    ],
    postFontSize: 1
  }
}
js
const posts = ref([
  /* ... */
])

const postFontSize = ref(1)

这可以在模板中使用来控制所有博客文章的字体大小

模板
<div :style="{ fontSize: postFontSize + 'em' }">
  <BlogPost
    v-for="post in posts"
    :key="post.id"
    :title="post.title"
   />
</div>

现在让我们在 <BlogPost> 组件的模板中添加一个按钮

vue
<!-- BlogPost.vue, omitting <script> -->
<template>
  <div class="blog-post">
    <h4>{{ title }}</h4>
    <button>Enlarge text</button>
  </div>
</template>

按钮目前没有任何功能 - 我们希望点击按钮能让父组件知道应该放大所有帖子的文本。为了解决这个问题,组件提供了一个自定义事件系统。父组件可以选择使用 v-on@ 来监听子组件实例上的任何事件,就像我们使用原生DOM事件一样

模板
<BlogPost
  ...
  @enlarge-text="postFontSize += 0.1"
 />

然后子组件可以通过调用内置的 $emit 方法 在自己上发出事件,传递事件的名称

vue
<!-- BlogPost.vue, omitting <script> -->
<template>
  <div class="blog-post">
    <h4>{{ title }}</h4>
    <button @click="$emit('enlarge-text')">Enlarge text</button>
  </div>
</template>

多亏了 @enlarge-text="postFontSize += 0.1" 监听器,父组件将接收到事件并更新 postFontSize 的值。

我们可以选择性地使用 emits 选项defineEmits 来声明发出的事件

vue
<!-- BlogPost.vue -->
<script>
export default {
  props: ['title'],
  emits: ['enlarge-text']
}
</script>
vue
<!-- BlogPost.vue -->
<script setup>
defineProps(['title'])
defineEmits(['enlarge-text'])
</script>

这记录了组件发出的所有事件,并可选择 验证它们。它还允许Vue避免将它们隐式地应用于子组件根元素的原生监听器。

defineProps 类似,defineEmits 只能在 <script setup> 中使用,且不需要导入。它返回一个等同于 $emit 方法的 emit 函数。它可以在组件的 <script setup> 部分中使用,在那里 $emit 不能直接访问

vue
<script setup>
const emit = defineEmits(['enlarge-text'])

emit('enlarge-text')
</script>

另请参阅: 组件发出事件的类型化

如果您不使用 <script setup>,您可以使用 emits 选项来声明发出的事件。您可以将 emit 函数作为 setup 上下文的一个属性访问,它作为第二个参数传递给 setup()

js
export default {
  emits: ['enlarge-text'],
  setup(props, ctx) {
    ctx.emit('enlarge-text')
  }
}

现在您需要了解关于自定义组件事件的全部内容,但是当您阅读完本页并对其内容感到熟悉后,我们建议您稍后回来阅读关于自定义事件的完整指南。

使用插槽进行内容分发

就像HTML元素一样,经常需要将内容传递给组件,如下所示

模板
<AlertBox>
  Something bad happened.
</AlertBox>

可能渲染出类似的内容

这是一个用于演示的错误

发生了某些事情。

这可以通过使用Vue的自定义<slot>元素实现

vue
<!-- AlertBox.vue -->
<template>
  <div class="alert-box">
    <strong>This is an Error for Demo Purposes</strong>
    <slot />
  </div>
</template>

<style scoped>
.alert-box {
  /* ... */
}
</style>

如您所看到的,我们使用<slot>作为内容要放置的占位符 - 这就完成了。我们已经完成了!

现在您需要了解关于插槽的全部内容,但是当您阅读完本页并对其内容感到熟悉后,我们建议您稍后回来阅读关于插槽的完整指南。

动态组件

有时,在标签页界面中动态切换组件是有用的

上面的内容是通过Vue的<component>元素和特殊的is属性实现的

模板
<!-- Component changes when currentTab changes -->
<component :is="currentTab"></component>
模板
<!-- Component changes when currentTab changes -->
<component :is="tabs[currentTab]"></component>

在上面的示例中,传递给:is的值可以是

  • 已注册组件的名称字符串,或者
  • 实际导入的组件对象

您还可以使用is属性来创建常规HTML元素。

当使用<component :is="...">在多个组件之间切换时,当切换到其他组件时,组件将被卸载。我们可以使用内置的<KeepAlive>组件强制非活动组件保持“活跃”。

in-DOM 模板解析注意事项

如果您直接在DOM中编写Vue模板,Vue将不得不从DOM中检索模板字符串。由于浏览器原生HTML解析行为,这导致了一些注意事项。

提示

以下讨论的限制仅适用于您直接在DOM中编写模板的情况。它们不适用于您从以下来源使用字符串模板的情况

  • 单文件组件
  • 内联模板字符串(例如 template: '...'
  • <script type="text/x-template">

不区分大小写

HTML标签和属性名称不区分大小写,因此浏览器将任何大写字符解释为小写。这意味着当您使用in-DOM模板时,PascalCase组件名称和camelCased属性名或v-on事件名都需要使用它们的kebab-cased(短横线分隔)等效名称

js
// camelCase in JavaScript
const BlogPost = {
  props: ['postTitle'],
  emits: ['updatePost'],
  template: `
    <h3>{{ postTitle }}</h3>
  `
}
模板
<!-- kebab-case in HTML -->
<blog-post post-title="hello!" @update-post="onUpdatePost"></blog-post>

自闭合标签

我们在之前的代码示例中使用了组件的自闭合标签

模板
<MyComponent />

这是因为Vue的模板解析器将/>视为结束任何标签的指示,无论其类型如何。

然而,在in-DOM模板中,我们必须始终包含显式的关闭标签

模板
<my-component></my-component>

这是因为在HTML规范中,只有少数几个特定元素可以省略闭合标签,最常见的有<input><img>。对于所有其他元素,如果你省略了闭合标签,原生的HTML解析器会认为你从未结束开标签。例如,以下代码片段

模板
<my-component /> <!-- we intend to close the tag here... -->
<span>hello</span>

会被解析为

模板
<my-component>
  <span>hello</span>
</my-component> <!-- but the browser will close it here. -->

元素放置限制

一些HTML元素,如<ul><ol><table><select>,对其内部可以出现哪些元素有限制,而一些元素如<li><tr><option>只能出现在某些其他元素内部。

这在使用具有此类限制的元素组件时会导致问题。例如

模板
<table>
  <blog-post-row></blog-post-row>
</table>

自定义组件<blog-post-row>将被提升为无效内容,导致最终渲染输出中的错误。我们可以使用特殊的is属性作为解决方案

模板
<table>
  <tr is="vue:blog-post-row"></tr>
</table>

提示

当用于原生HTML元素时,is的值必须以vue:为前缀,才能被视为Vue组件。这是为了避免与原生的自定义内置元素混淆。

这就是目前你需要了解的DOM模板解析的注意事项——实际上,也是Vue的《基础知识》的结束。恭喜!还有很多东西要学,但首先,我们建议你先休息一下,亲自尝试一下Vue——构建一些有趣的东西,或者查看一些示例,如果你还没有的话。

一旦你对你刚刚吸收的知识感到自信,继续阅读指南,深入学习组件。

组件基础知识已加载