Vue3 组件系统详解
概述
Vue3 的组件系统让你可以把页面拆分成多个可复用的部分。每个组件都有自己的数据、方法和模板,可以独立开发和测试。
组件通信方式
组件注册
1. 全局注册
// main.js
import { createApp } from "vue";
import App from "./App.vue";
import MyComponent from "./components/MyComponent.vue";
const app = createApp(App);
app.component("MyComponent", MyComponent);
app.mount("#app");
2. 局部注册
// ParentComponent.vue
<template>
<div>
<h1>父组件</h1>
<ChildComponent :message="parentMessage" @update="handleUpdate" />
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue'
export default {
components: {
ChildComponent
},
setup() {
const parentMessage = ref('来自父组件的消息')
const handleUpdate = (newMessage) => {
parentMessage.value = newMessage
}
return {
parentMessage,
handleUpdate
}
}
}
</script>
Props 传递数据
1. 基本用法
// ChildComponent.vue
<template>
<div>
<h2>子组件</h2>
<p>接收到的消息: {{ message }}</p>
<p>用户年龄: {{ user.age }}</p>
<p>是否显示: {{ show ? '是' : '否' }}</p>
</div>
</template>
<script>
export default {
props: {
// 基本类型
message: {
type: String,
required: true,
default: '默认消息'
},
// 对象类型
user: {
type: Object,
default: () => ({
name: '默认用户',
age: 0
})
},
// 布尔类型
show: {
type: Boolean,
default: false
}
},
setup(props) {
// 在 setup 中访问 props
console.log('消息:', props.message)
console.log('用户:', props.user)
console.log('显示状态:', props.show)
return {}
}
}
</script>
2. Props 验证
export default {
props: {
// 多种类型
value: [String, Number],
// 自定义验证函数
age: {
type: Number,
validator: (value) => {
return value >= 0 && value <= 150;
},
},
// 带默认值的函数
items: {
type: Array,
default: () => [],
},
},
};
事件通信
1. 子组件向父组件发送事件
// ChildComponent.vue
<template>
<div>
<button @click="sendMessage">发送消息给父组件</button>
<button @click="updateCount">更新计数</button>
</div>
</template>
<script>
export default {
emits: ['message', 'update:count'], // 声明要触发的事件
setup(props, { emit }) {
const sendMessage = () => {
emit('message', '来自子组件的消息')
}
const updateCount = () => {
emit('update:count', 42)
}
return {
sendMessage,
updateCount
}
}
}
</script>
2. 父组件监听事件
// ParentComponent.vue
<template>
<div>
<ChildComponent
@message="handleMessage"
@update:count="handleCountUpdate"
/>
<p>收到的消息: {{ receivedMessage }}</p>
<p>计数: {{ count }}</p>
</div>
</template>
<script>
export default {
setup() {
const receivedMessage = ref('')
const count = ref(0)
const handleMessage = (message) => {
receivedMessage.value = message
}
const handleCountUpdate = (newCount) => {
count.value = newCount
}
return {
receivedMessage,
count,
handleMessage,
handleCountUpdate
}
}
}
</script>
Provide/Inject
用于跨层级组件通信,避免 props 逐层传递。
1. 提供数据
// AncestorComponent.vue
<template>
<div>
<h1>祖先组件</h1>
<ParentComponent />
</div>
</template>
<script>
import { provide, ref } from 'vue'
import ParentComponent from './ParentComponent.vue'
export default {
components: { ParentComponent },
setup() {
const theme = ref('light')
const user = ref({
name: '张三',
role: 'admin'
})
// 提供响应式数据
provide('theme', theme)
provide('user', user)
// 提供方法
provide('updateTheme', (newTheme) => {
theme.value = newTheme
})
return {}
}
}
</script>
2. 注入数据
// DeepChildComponent.vue
<template>
<div>
<h3>深层子组件</h3>
<p>主题: {{ theme }}</p>
<p>用户: {{ user.name }} ({{ user.role }})</p>
<button @click="switchTheme">切换主题</button>
</div>
</template>
<script>
import { inject } from 'vue'
export default {
setup() {
// 注入数据,提供默认值
const theme = inject('theme', 'default-theme')
const user = inject('user', { name: '未知用户', role: 'guest' })
const updateTheme = inject('updateTheme', () => {})
const switchTheme = () => {
updateTheme(theme === 'light' ? 'dark' : 'light')
}
return {
theme,
user,
switchTheme
}
}
}
</script>
插槽 (Slots)
插槽让父组件可以向子组件传递内容。
1. 默认插槽
// ChildComponent.vue
<template>
<div class="card">
<div class="card-header">
<slot name="header">
<h3>默认标题</h3>
</slot>
</div>
<div class="card-body">
<slot>这是默认内容</slot>
</div>
<div class="card-footer">
<slot name="footer">
<button>默认按钮</button>
</slot>
</div>
</div>
</template>
2. 使用插槽
// ParentComponent.vue
<template>
<div>
<ChildComponent>
<!-- 具名插槽 -->
<template #header>
<h3>自定义标题</h3>
</template>
<!-- 默认插槽 -->
<p>这是自定义内容</p>
<ul>
<li>列表项 1</li>
<li>列表项 2</li>
</ul>
<!-- 具名插槽 -->
<template #footer>
<button @click="handleClick">自定义按钮</button>
</template>
</ChildComponent>
</div>
</template>
3. 作用域插槽
// ChildComponent.vue
<template>
<div>
<slot
:items="items"
:loading="loading"
:error="error"
>
<p>默认内容</p>
</slot>
</div>
</template>
<script>
export default {
setup() {
const items = ref(['苹果', '香蕉', '橙子'])
const loading = ref(false)
const error = ref(null)
return {
items,
loading,
error
}
}
}
</script>
// ParentComponent.vue
<template>
<div>
<ChildComponent v-slot="{ items, loading, error }">
<div v-if="loading">加载中...</div>
<div v-else-if="error">错误: {{ error }}</div>
<ul v-else>
<li v-for="item in items" :key="item">{{ item }}</li>
</ul>
</ChildComponent>
</div>
</template>
动态组件
<template>
<div>
<button @click="currentComponent = 'ComponentA'">显示组件A</button>
<button @click="currentComponent = 'ComponentB'">显示组件B</button>
<component :is="currentComponent" />
</div>
</template>
<script>
import ComponentA from './ComponentA.vue'
import ComponentB from './ComponentB.vue'
export default {
components: {
ComponentA,
ComponentB
},
setup() {
const currentComponent = ref('ComponentA')
return {
currentComponent
}
}
}
</script>
异步组件
// 懒加载组件
const AsyncComponent = defineAsyncComponent(() =>
import("./HeavyComponent.vue")
);
// 带加载和错误状态的异步组件
const AsyncComponentWithStates = defineAsyncComponent({
loader: () => import("./HeavyComponent.vue"),
loadingComponent: LoadingSpinner,
errorComponent: ErrorDisplay,
delay: 200,
timeout: 3000,
});
组件最佳实践
1. 单一职责原则
- 每个组件只负责一个功能
- 保持组件的简洁性
- 避免组件过于复杂
2. 命名规范
- 组件名使用 PascalCase
- 文件名与组件名保持一致
- 使用有意义的名称
3. Props 设计
- 使用 TypeScript 类型定义
- 提供合理的默认值
- 添加必要的验证
4. 事件设计
- 使用 kebab-case 命名事件
- 在 emits 选项中声明事件
- 提供清晰的事件参数
总结
Vue3 的组件系统提供了强大的组件化开发能力。通过合理使用 props、事件、插槽等通信方式,可以构建出模块化、可复用的组件库。掌握这些概念和最佳实践,将帮助你构建出更好的 Vue 应用。
