Vue 插槽系统
插槽(Slot)是 Vue 组件化中非常重要的概念,它允许你在组件中预留位置,让父组件可以向子组件的内容区插入自定义内容。
基本概念
什么是插槽?
插槽用于让组件的模板中可以包含父组件提供的内容:
┌─────────────────────────────────────────────────────────┐
│ 父组件 │
│ ┌─────────────────────────────────────────────────┐ │
│ │ <Layout> │ │
│ │ <template #header>页眉内容</template> │ │
│ │ <template #default>主内容</template> │ │
│ │ <template #footer>页脚内容</template> │ │
│ │ </Layout> │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 子组件 Layout │
│ ┌─────────────────────────────────────────────────┐ │
│ │ <header><slot name="header" /></header> │ │
│ │ <main><slot /></main> │ │
│ │ <footer><slot name="footer" /></footer> │ │
│ └─────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
默认插槽
基本用法
<!-- MyComponent.vue -->
<template>
<div class="container">
<slot>这是默认内容</slot>
</div>
</template>
<!-- 父组件 -->
<template>
<MyComponent>
这是插入到插槽的内容
</MyComponent>
</template>
渲染结果
<div class="container">
这是插入到插槽的内容
</div>
具名插槽
当一个组件中有多个插槽时,需要给插槽起名字:
定义具名插槽
<!-- BaseLayout.vue -->
<div class="layout">
<header>
<slot name="header">默认页眉</slot>
</header>
<main>
<slot>默认内容</slot>
</main>
<footer>
<slot name="footer">默认页脚</slot>
</footer>
</div>
使用具名插槽
<!-- 父组件 -->
<template>
<BaseLayout>
<template #header>
<h1>网站标题</h1>
</template>
<template #default>
<p>主内容区域</p>
</template>
<template #footer>
<p>版权所有 2024</p>
</template>
</BaseLayout>
</template>
提示
#header 是 v-slot:header 的简写形式。
作用域插槽
作用域插槽允许子组件向父组件暴露数据,父组件可以根据这些数据进行渲染。
子组件暴露数据
<!-- Child.vue -->
<script setup>
import { ref } from 'vue'
const items = ref([
{ id: 1, name: '苹果' },
{ id: 2, name: '香蕉' },
{ id: 3, name: '橙子' }
])
</script>
<template>
<ul>
<li v-for="item in items" :key="item.id">
<slot :item="item" />
</li>
</ul>
</template>
父组件接收数据
<!-- 父组件 -->
<template>
<Child>
<template #default="{ item }">
{{ item.name }} (ID: {{ item.id }})
</template>
</Child>
</template>
动态插槽名
插槽名可以是动态的:
<template>
<div>
<slot :name="dynamicSlotName"></slot>
</div>
</template>
<script setup>
const dynamicSlotName = 'content'
</script>
<template>
<MyComponent>
<template #[dynamicSlotName]>
动态插槽内容
</template>
</MyComponent>
</template>
插槽的高级用法
解构插槽 Props
<!-- 父组件 -->
<template>
<Child>
<template #default="{ item, index, last }">
<span :class="{ 'last-item': last }">
{{ index + 1 }}. {{ item.name }}
</span>
</template>
</Child>
</template>
备用内容(Fallback)
<!-- 子组件 -->
<template>
<button type="submit">
<slot>提交</slot> <!-- 默认文字是"提交" -->
</button>
</template>
使用时不提供内容,将使用默认内容:
<MyButton />
提供内容时,将替换默认内容:
<MyButton>立即注册</MyButton>
示例:Card 卡片组件
<!-- Card.vue -->
<script setup>
defineProps({
title: String,
shadow: {
type: String,
default: 'always', // always, hover, never
}
})
</script>
<template>
<div class="card" :class="`shadow-${shadow}`">
<div v-if="$slots.header || title" class="card-header">
<slot name="header">
<h3>{{ title }}</h3>
</slot>
</div>
<div class="card-body">
<slot></slot>
</div>
<div v-if="$slots.footer" class="card-footer">
<slot name="footer"></slot>
</div>
</div>
</template>
<style scoped>
.card {
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
}
.shadow-always { box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
.shadow-hover:hover { box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
.card-header {
padding: 16px;
border-bottom: 1px solid #e0e0e0;
background: #f5f5f5;
}
.card-body { padding: 16px; }
.card-footer {
padding: 16px;
border-top: 1px solid #e0e0e0;
background: #f9f9f9;
}
</style>
使用 Card 组件
<template>
<div class="cards">
<Card title="卡片标题">
<p>卡片内容区域</p>
<p>可以放置任意内容</p>
</Card>
<Card>
<template #header>
<h3>自定义头部</h3>
</template>
<p>自定义内容</p>
<template #footer>
<button>操作按钮</button>
</template>
</Card>
<Card shadow="hover">
<p>Hover 时显示阴影</p>
</Card>
</div>
</template>
<style scoped>
.cards {
display: flex;
gap: 20px;
flex-wrap: wrap;
}
</style>
示例:Table 表格组件
<!-- Table.vue -->
<script setup>
defineProps({
columns: Array,
data: Array
})
</script>
<template>
<table class="table">
<thead>
<tr>
<th v-for="col in columns" :key="col.key">
{{ col.label }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, index) in data" :key="index">
<td v-for="col in columns" :key="col.key">
<slot :name="col.key" :row="row" :value="row[col.key]">
{{ row[col.key] }}
</slot>
</td>
</tr>
</tbody>
</table>
</template>
<style scoped>
.table {
width: 100%;
border-collapse: collapse;
}
.table th, .table td {
border: 1px solid #ddd;
padding: 8px;
}
.table th { background: #f5f5f5; }
</style>
使用 Table 组件
<script setup>
const columns = [
{ key: 'name', label: '姓名' },
{ key: 'age', label: '年龄' },
{ key: 'action', label: '操作' }
]
const data = [
{ name: '张三', age: 25 },
{ name: '李四', age: 30 }
]
</script>
<template>
<Table :columns="columns" :data="data">
<template #action="{ row }">
<button @click="edit(row)">编辑</button>
<button @click="delete(row)">删除</button>
</template>
</Table>
</template>
小结
本章我们详细学习了 Vue 插槽系统的完整内容:
- 默认插槽:最简单的插槽用法
- 具名插槽:多个插槽时使用
- 作用域插槽:子组件向父组件传递数据
- 动态插槽名:插槽名可以是动态的
- 备用内容:插槽的默认值
- 实际示例:Card 和 Table 组件
练习
- 创建一个 Accordion 手风琴组件
- 创建一个 List 列表组件,支持自定义渲染
- 创建一个 Modal 对话框组件
准备好进入下一章,学习组合式 API 的内容了吗?