组件基础
组件允许我们将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——构建一些有趣的东西,或者查看一些示例,如果你还没有的话。
一旦你对你刚刚吸收的知识感到自信,继续阅读指南,深入学习组件。