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

这与我们嵌套原生的HTML元素非常相似,但Vue实现了自己的组件模型,允许我们在每个组件中封装自定义内容和逻辑。Vue也与原生的Web组件很好地配合。如果您对Vue组件和原生Web组件之间的关系感兴趣,请在此了解更多。
定义组件
当使用构建步骤时,我们通常使用.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>当不使用构建步骤时,Vue组件可以定义为一个包含Vue特定选项的普通JavaScript对象
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 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 列表中声明它,使用 defineProps 宏
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
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
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 的值。
我们可以选择性地使用 defineEmits 宏 来声明发出的事件
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="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——构建一些有趣的东西,或者查看一些示例,如果你还没有的话。
一旦你对你刚刚吸收的知识感到自信,继续阅读指南,深入学习组件。