跳到主要内容

Vue 组件基础

组件(Component)是 Vue 最强大的功能之一。组件可以将 UI 拆分为独立可复用的 pieces,并让你对每个 piece 进行独立开发。

什么是组件?

组件是可复用的 Vue 实例,拥有独立的模板、逻辑和样式。比如,一个按钮组件可以在多个地方使用:

<MyButton />
<MyButton />
<MyButton />

创建组件

单文件组件

Vue 推荐使用单文件组件(.vue)来组织代码:

<!-- MyComponent.vue -->
<script setup>
// 组件逻辑
</script>

<template>
<!-- 组件模板 -->
</template>

<style scoped>
/* 组件样式 */
</style>

组件命名规范

  • PascalCase(帕斯卡命名):推荐,如 MyComponent
  • kebab-case(短横线命名):HTML 中使用,如 my-component

注册组件

全局注册

main.js 中全局注册组件:

import { createApp } from 'vue'
import App from './App.vue'
import MyButton from './components/MyButton.vue'

const app = createApp(App)
app.component('MyButton', MyButton)
app.mount('#app')

全局注册的组件可以在任何地方使用。

局部注册

在组件内部导入并使用:

<script setup>
import MyButton from './components/MyButton.vue'
import MyCard from './components/MyCard.vue'
</script>

<template>
<MyButton />
<MyCard />
</template>

使用组件

基本用法

<!-- App.vue -->
<script setup>
import MyComponent from './components/MyComponent.vue'
</script>

<template>
<MyComponent />
</template>

传递 Props

父组件向子组件传递数据:

<!-- 子组件: MyButton.vue -->
<script setup>
defineProps({
text: {
type: String,
default: '按钮'
},
type: {
type: String,
default: 'primary'
}
})
</script>

<template>
<button :class="type">{{ text }}</button>
</template>

<style scoped>
.primary {
background: #42b883;
color: white;
}
.secondary {
background: #666;
color: white;
}
</style>
<!-- 父组件 -->
<template>
<MyButton text="登录" type="primary" />
<MyButton text="取消" type="secondary" />
</template>

监听事件

子组件向父组件传递消息:

<!-- 子组件: MyDialog.vue -->
<script setup>
const emit = defineEmits(['close', 'confirm'])

function handleClose() {
emit('close')
}
</script>

<template>
<div class="dialog">
<p>这是一个对话框</p>
<button @click="handleClose">关闭</button>
</div>
</template>
<!-- 父组件 -->
<template>
<MyDialog @close="handleClose" @confirm="handleConfirm" />
</template>

<script setup>
function handleClose() {
console.log('对话框关闭')
}
function handleConfirm() {
console.log('确认')
}
</script>

组件示例:计数器

<!-- Counter.vue -->
<script setup>
import { ref } from 'vue'

const count = ref(0)

function increment() {
count.value++
}
</script>

<template>
<div class="counter">
<h3>计数器: {{ count }}</h3>
<button @click="increment">增加</button>
</div>
</template>

<style scoped>
.counter {
text-align: center;
padding: 20px;
}
button {
padding: 8px 16px;
background: #42b883;
color: white;
border: none;
cursor: pointer;
}
</style>

组件示例:用户卡片

<!-- UserCard.vue -->
<script setup>
defineProps({
name: String,
email: String,
avatar: String
})
</script>

<template>
<div class="user-card">
<img :src="avatar || 'https://via.placeholder.com/100'" alt="头像" />
<h3>{{ name }}</h3>
<p>{{ email }}</p>
</div>
</template>

<style scoped>
.user-card {
text-align: center;
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
max-width: 200px;
}
img {
width: 100px;
height: 100px;
border-radius: 50%;
}
</style>
<!-- 使用 -->
<template>
<UserCard
name="张三"
email="[email protected]"
avatar="https://i.pravatar.cc/100"
/>
</template>

内容分发(插槽)

有时候我们需要向组件传递内容,而不仅仅是数据。Vue 使用 <slot> 元素来实现内容分发:

基本插槽

<!-- AlertBox.vue -->
<script setup>
</script>

<template>
<div class="alert-box">
<strong>错误提示</strong>
<slot />
</div>
</template>

<style scoped>
.alert-box {
padding: 15px;
background: #fff2f0;
border: 1px solid #ffccc7;
border-radius: 4px;
}
</style>
<!-- 使用 -->
<template>
<AlertBox>
发生了一些错误,请检查您的输入。
</AlertBox>
</template>

<slot> 是一个占位符,父组件传入的内容会替换这个位置。

具名插槽

当需要多个插槽时,可以使用具名插槽:

<!-- Layout.vue -->
<script setup>
</script>

<template>
<div class="layout">
<header>
<slot name="header" />
</header>
<main>
<slot />
</main>
<footer>
<slot name="footer" />
</footer>
</div>
</template>
<!-- 使用 -->
<template>
<Layout>
<template #header>
<h1>页面标题</h1>
</template>

<p>主要内容放在这里</p>

<template #footer>
<p>版权信息</p>
</template>
</Layout>
</template>

动态组件

有时候我们需要在不同组件之间动态切换,比如标签页界面。Vue 提供了 <component> 元素配合 is 属性来实现:

基本用法

<script setup>
import { ref } from 'vue'
import Home from './Home.vue'
import Posts from './Posts.vue'
import Archive from './Archive.vue'

const currentTab = ref('Home')
const tabs = {
Home,
Posts,
Archive
}
</script>

<template>
<div class="demo">
<!-- 标签按钮 -->
<button
v-for="(_, tab) in tabs"
:key="tab"
:class="['tab-button', { active: currentTab === tab }]"
@click="currentTab = tab"
>
{{ tab }}
</button>

<!-- 动态组件 -->
<component :is="tabs[currentTab]" class="tab-content" />
</div>
</template>

<style scoped>
.tab-button {
padding: 6px 10px;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
border: 1px solid #ccc;
cursor: pointer;
background: #f0f0f0;
margin-bottom: -1px;
margin-right: 5px;
}
.tab-button.active {
background: #e0e0e0;
}
.tab-content {
padding: 10px;
border: 1px solid #ccc;
}
</style>

:is 的值类型

传递给 :is 的值可以是:

<script setup>
import { ref } from 'vue'
import ComponentA from './ComponentA.vue'

const current = ref(ComponentA)
const componentName = ref('ComponentA')
</script>

<template>
<!-- 1. 直接传递组件对象 -->
<component :is="ComponentA" />

<!-- 2. 传递组件的引用 -->
<component :is="current" />

<!-- 3. 传递组件名字符串(需要全局注册) -->
<component :is="componentName" />

<!-- 4. 也可以是普通 HTML 元素 -->
<component :is="'div'" class="container">内容</component>
</template>

KeepAlive 保持状态

默认情况下,切换动态组件会卸载旧组件并挂载新组件,组件状态会丢失。使用 <KeepAlive> 可以缓存组件实例:

<script setup>
import { ref } from 'vue'
import Home from './Home.vue'
import Posts from './Posts.vue'

const currentTab = ref('Home')
const tabs = { Home, Posts }
</script>

<template>
<button @click="currentTab = 'Home'">Home</button>
<button @click="currentTab = 'Posts'">Posts</button>

<!-- 使用 KeepAlive 缓存组件 -->
<KeepAlive>
<component :is="tabs[currentTab]" />
</KeepAlive>
</template>

KeepAlive 的效果

<!-- Counter.vue - 带状态的组件 -->
<script setup>
import { ref } from 'vue'

const count = ref(0)
</script>

<template>
<p>计数: {{ count }}</p>
<button @click="count++">增加</button>
</template>
<!-- App.vue -->
<script setup>
import { ref } from 'vue'
import Counter from './Counter.vue'

const show = ref(true)
</script>

<template>
<button @click="show = !show">切换</button>

<!-- 不使用 KeepAlive:切换后 count 会重置为 0 -->
<Counter v-if="show" />

<!-- 使用 KeepAlive:切换后 count 保持原值 -->
<KeepAlive>
<Counter v-if="show" />
</KeepAlive>
</template>

KeepAlive 配置选项

<script setup>
import { ref } from 'vue'
import CompA from './CompA.vue'
import CompB from './CompB.vue'
import CompC from './CompC.vue'

const current = ref(CompA)
</script>

<template>
<!-- include:只缓存指定组件 -->
<KeepAlive include="CompA,CompB">
<component :is="current" />
</KeepAlive>

<!-- exclude:不缓存指定组件 -->
<KeepAlive exclude="CompC">
<component :is="current" />
</KeepAlive>

<!-- max:最大缓存实例数(使用 LRU 策略) -->
<KeepAlive :max="10">
<component :is="current" />
</KeepAlive>

<!-- 使用正则表达式 -->
<KeepAlive :include="/Comp[A-Z]/">
<component :is="current" />
</KeepAlive>
</template>

DOM 模板解析注意事项

如果你在 HTML 文件中直接编写 Vue 模板(而不是 .vue 文件),需要注意浏览器原生 HTML 解析的行为:

大小写不敏感

HTML 标签和属性名不区分大小写,浏览器会将大写字母转为小写:

<!-- 错误:在 DOM 模板中 -->
<BlogPost postTitle="hello" @updatePost="onUpdate" />

<!-- 正确:使用 kebab-case -->
<blog-post post-title="hello" @update-post="onUpdate" />

自闭合标签

HTML 规范只允许少数特定元素省略闭合标签(如 <input><img>)。其他元素必须显式闭合:

<!-- 在 .vue 文件中可以使用自闭合 -->
<MyComponent />

<!-- 在 DOM 模板中必须显式闭合 -->
<my-component></my-component>

元素放置限制

某些 HTML 元素有嵌套限制,比如 <ul> 只能包含 <li><table> 只能包含特定元素:

<!-- 错误:自定义组件会被提升到外面 -->
<table>
<blog-post-row></blog-post-row>
</table>

<!-- 正确:使用 is 属性 -->
<table>
<tr is="vue:blog-post-row"></tr>
</table>
注意

在原生 HTML 元素上使用 is 属性时,必须添加 vue: 前缀,以便与原生自定义内置元素区分。

组件 v-model

组件上也可以使用 v-model 实现双向绑定:

基本用法

<!-- CustomInput.vue -->
<script setup>
const model = defineModel()
</script>

<template>
<input v-model="model" />
</template>
<!-- 使用 -->
<script setup>
import { ref } from 'vue'
import CustomInput from './CustomInput.vue'

const text = ref('')
</script>

<template>
<CustomInput v-model="text" />
<p>输入内容: {{ text }}</p>
</template>

多个 v-model

<!-- UserName.vue -->
<script setup>
const firstName = defineModel('firstName')
const lastName = defineModel('lastName')
</script>

<template>
<input v-model="firstName" placeholder="名" />
<input v-model="lastName" placeholder="姓" />
</template>
<!-- 使用 -->
<template>
<UserName v-model:first-name="first" v-model:last-name="last" />
</template>

小结

本章我们学习了 Vue 组件的基础知识:

  1. 组件概念:可复用的 Vue 实例
  2. 单文件组件:.vue 文件格式
  3. 组件注册:全局和局部注册
  4. Props 传递:父组件向子组件传数据
  5. 事件监听:子组件向父组件传消息

练习

  1. 创建一个 ProductsList 组件,显示商品列表
  2. 创建一个 Modal 组件,支持 open 和 close 事件
  3. 创建一个 FormInput 组件,支持 label 和 error 属性

准备好进入下一章,学习组件通信的内容了吗?