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 组件的基础知识:
- 组件概念:可复用的 Vue 实例
- 单文件组件:.vue 文件格式
- 组件注册:全局和局部注册
- Props 传递:父组件向子组件传数据
- 事件监听:子组件向父组件传消息
练习
- 创建一个 ProductsList 组件,显示商品列表
- 创建一个 Modal 组件,支持 open 和 close 事件
- 创建一个 FormInput 组件,支持 label 和 error 属性
准备好进入下一章,学习组件通信的内容了吗?