插槽是 Vue 中一种强大的内容分发机制,允许组件接收外部传入的内容并进行渲染。下面我通过简单易懂的示例详细说明。
📚 插槽的基本概念
定义 :插槽是组件内部预留的"占位符",允许父组件向子组件传递模板内容(HTML结构、组件等)。
类比理解:就像电脑的 USB 接口(插槽),你可以插入 U盘、鼠标、键盘等(内容),接口本身是固定的,但插入的内容可以变化。
🔧 三种插槽类型
| 类型 | 数量 | 特点 | 使用场景 |
|---|---|---|---|
| 默认插槽 | 1个 | 没有名称 | 通用内容分发 |
| 具名插槽 | 多个 | 有名称标识 | 多个内容位置 |
| 作用域插槽 | 不限 | 可传递数据 | 子组件向父组件传数据 |
一、默认插槽
基本示例
vue
<!-- ChildComponent.vue -->
<template>
<div class="card">
<div class="card-header">
<!-- 默认插槽:没有 name 属性 -->
<slot></slot>
</div>
<div class="card-body">
<p>这是卡片的内容区域</p>
</div>
</div>
</template>
vue
<!-- ParentComponent.vue -->
<template>
<div>
<!-- 使用子组件 -->
<ChildComponent>
<!-- 这里的内容会替换子组件中的 <slot></slot> -->
<h2>自定义标题</h2>
<p>这是自定义的内容</p>
</ChildComponent>
<!-- 也可以传递组件 -->
<ChildComponent>
<CustomButton>点击我</CustomButton>
</ChildComponent>
<!-- 不传内容时显示默认内容 -->
<ChildComponent></ChildComponent>
</div>
</template>
输出结果:
html
<div class="card">
<div class="card-header">
<h2>自定义标题</h2>
<p>这是自定义的内容</p>
</div>
<div class="card-body">
<p>这是卡片的内容区域</p>
</div>
</div>
带默认内容的插槽
vue
<!-- ChildComponent.vue -->
<template>
<div class="notification">
<!-- 默认内容:当父组件不传内容时显示 -->
<slot>
<p>默认通知:没有新消息</p>
</slot>
</div>
</template>
vue
<!-- ParentComponent.vue -->
<template>
<div>
<!-- 不传内容,显示默认 -->
<NotificationComponent />
<!-- 传递自定义内容 -->
<NotificationComponent>
<p style="color: red;">⚠️ 有紧急消息!</p>
</NotificationComponent>
</div>
</template>
二、具名插槽
基本用法
vue
<!-- LayoutComponent.vue -->
<template>
<div class="layout">
<!-- 具名插槽:有 name 属性 -->
<header>
<slot name="header"></slot>
</header>
<main>
<!-- 默认插槽(可以省略 name 或写成 name="default") -->
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
<aside>
<slot name="sidebar"></slot>
</aside>
</div>
</template>
<style scoped>
.layout {
display: grid;
grid-template-areas:
"header header"
"sidebar main"
"footer footer";
grid-template-columns: 200px 1fr;
gap: 20px;
}
header { grid-area: header; background: #f0f0f0; padding: 20px; }
main { grid-area: main; background: white; padding: 20px; }
footer { grid-area: footer; background: #333; color: white; padding: 20px; }
aside { grid-area: sidebar; background: #f5f5f5; padding: 20px; }
</style>
vue
<!-- ParentComponent.vue -->
<template>
<LayoutComponent>
<!-- 使用 v-slot:name 或 #name 指定插槽 -->
<template v-slot:header>
<h1>网站标题</h1>
<nav>
<a href="/">首页</a>
<a href="/about">关于</a>
</nav>
</template>
<!-- 简写:使用 # 代替 v-slot: -->
<template #sidebar>
<h3>侧边栏</h3>
<ul>
<li>菜单1</li>
<li>菜单2</li>
</ul>
</template>
<!-- 默认插槽(不指定名字的内容) -->
<h2>主要内容区域</h2>
<p>这里是页面的主要内容...</p>
<template #footer>
<p>© 2024 版权所有</p>
<p>联系邮箱:contact@example.com</p>
</template>
</LayoutComponent>
</template>
多种写法对比
vue
<!-- 完整写法 -->
<template v-slot:header>内容</template>
<!-- 简写1 -->
<template #header>内容</template>
<!-- 简写2(动态插槽名) -->
<template #[dynamicSlotName]>内容</template>
<!-- 简写3(在组件上使用,不推荐) -->
<LayoutComponent #header>内容</LayoutComponent>
三、作用域插槽
基本概念
作用域插槽允许子组件向父组件传递数据,父组件可以使用这些数据来渲染插槽内容。
vue
<!-- UserList.vue (子组件) -->
<template>
<div class="user-list">
<h3>用户列表</h3>
<ul>
<li v-for="user in users" :key="user.id">
<!-- 向插槽传递数据(user 和 index) -->
<slot :user="user" :index="$index">
<!-- 默认显示 -->
{{ user.name }} ({{ user.email }})
</slot>
</li>
</ul>
</div>
</template>
<script setup>
import { ref } from 'vue'
const users = ref([
{ id: 1, name: '张三', email: 'zhang@example.com', age: 25 },
{ id: 2, name: '李四', email: 'li@example.com', age: 30 },
{ id: 3, name: '王五', email: 'wang@example.com', age: 28 }
])
</script>
vue
<!-- ParentComponent.vue (父组件) -->
<template>
<div>
<h2>用户管理</h2>
<!-- 使用作用域插槽 -->
<UserList v-slot="{ user, index }">
<div class="user-item">
<strong>{{ index + 1 }}. {{ user.name }}</strong>
<p>邮箱: {{ user.email }}</p>
<p>年龄: {{ user.age }}</p>
<button @click="selectUser(user)">选择</button>
</div>
</UserList>
<!-- 另一种写法:指定插槽名 -->
<UserList>
<template #default="{ user, index }">
<div>
{{ index }} - {{ user.name }} ({{ user.age }}岁)
</div>
</template>
</UserList>
</div>
</template>
<script setup>
import UserList from './UserList.vue'
const selectUser = (user) => {
console.log('选中用户:', user)
}
</script>
实际应用示例:表格组件
vue
<!-- DataTable.vue -->
<template>
<div class="data-table">
<table>
<thead>
<tr>
<!-- 表头插槽 -->
<slot name="header">
<!-- 默认表头 -->
<th v-for="column in columns" :key="column.key">
{{ column.title }}
</th>
</slot>
</tr>
</thead>
<tbody>
<tr v-for="(row, index) in data" :key="row.id">
<!-- 行数据插槽 -->
<slot :row="row" :index="index">
<!-- 默认显示:显示所有列 -->
<td v-for="column in columns" :key="column.key">
{{ row[column.key] }}
</td>
</slot>
</tr>
</tbody>
</table>
</div>
</template>
<script setup>
defineProps({
data: {
type: Array,
required: true
},
columns: {
type: Array,
default: () => []
}
})
</script>
<style scoped>
.data-table table {
width: 100%;
border-collapse: collapse;
}
.data-table th, .data-table td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
.data-table th {
background-color: #f5f5f5;
}
</style>
vue
<!-- 使用表格组件 -->
<template>
<DataTable :data="users" :columns="columns">
<!-- 自定义表头 -->
<template #header>
<th>序号</th>
<th>姓名</th>
<th>年龄</th>
<th>操作</th>
</template>
<!-- 自定义行内容 -->
<template v-slot="{ row, index }">
<td>{{ index + 1 }}</td>
<td>
<strong>{{ row.name }}</strong>
<small>{{ row.email }}</small>
</td>
<td :class="{ 'young': row.age < 30 }">
{{ row.age }}
<span v-if="row.age < 30">👶</span>
<span v-else>🧓</span>
</td>
<td>
<button @click="editUser(row)">编辑</button>
<button @click="deleteUser(row)">删除</button>
</td>
</template>
</DataTable>
</template>
<script setup>
import { ref } from 'vue'
import DataTable from './DataTable.vue'
const users = ref([
{ id: 1, name: '张三', email: 'zhang@example.com', age: 25 },
{ id: 2, name: '李四', email: 'li@example.com', age: 35 },
{ id: 3, name: '王五', email: 'wang@example.com', age: 28 }
])
const columns = ref([
{ key: 'name', title: '姓名' },
{ key: 'email', title: '邮箱' },
{ key: 'age', title: '年龄' }
])
const editUser = (user) => {
console.log('编辑用户:', user)
}
const deleteUser = (user) => {
console.log('删除用户:', user)
}
</script>
<style>
.young {
color: green;
font-weight: bold;
}
</style>
四、高级用法
1. 动态插槽名
vue
<!-- DynamicSlotComponent.vue -->
<template>
<div>
<slot name="dynamicContent"></slot>
</div>
</template>
<script setup>
import { ref } from 'vue'
// 动态插槽名
const slotName = ref('contentA')
</script>
vue
<!-- 使用动态插槽 -->
<template>
<DynamicSlotComponent>
<!-- 动态决定使用哪个插槽 -->
<template #[slotName]>
动态内容
</template>
</DynamicSlotComponent>
</template>
<script setup>
import { ref } from 'vue'
const slotName = ref('dynamicContent')
</script>
2. 嵌套插槽
vue
<!-- LayoutWrapper.vue -->
<template>
<div class="wrapper">
<slot name="wrapperHeader">
<h2>包装器头部</h2>
</slot>
<!-- 嵌套:将插槽传递给子组件 -->
<BaseLayout>
<!-- 将父组件的内容传递给子组件的插槽 -->
<template v-for="(_, name) in $slots" #[name]="slotData">
<slot :name="name" v-bind="slotData"></slot>
</template>
</BaseLayout>
<slot name="wrapperFooter">
<p>包装器底部</p>
</slot>
</div>
</template>
3. 插槽函数
vue
<!-- Renderless组件(无渲染组件) -->
<template>
<!-- 只提供逻辑,不渲染任何内容 -->
<slot :data="data" :loading="loading" :error="error"></slot>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const data = ref(null)
const loading = ref(false)
const error = ref(null)
onMounted(async () => {
loading.value = true
try {
const response = await fetch('/api/data')
data.value = await response.json()
} catch (e) {
error.value = e
} finally {
loading.value = false
}
})
</script>
vue
<!-- 使用无渲染组件 -->
<template>
<DataFetcher v-slot="{ data, loading, error }">
<div v-if="loading">加载中...</div>
<div v-else-if="error">错误: {{ error.message }}</div>
<div v-else>
<h3>数据:</h3>
<pre>{{ data }}</pre>
</div>
</DataFetcher>
</template>
五、实际项目示例
示例1:模态框组件
vue
<!-- Modal.vue -->
<template>
<div v-if="visible" class="modal-overlay" @click.self="close">
<div class="modal">
<!-- 标题插槽 -->
<div class="modal-header">
<slot name="header">
<h3>提示</h3>
</slot>
<button class="close-btn" @click="close">×</button>
</div>
<!-- 内容插槽 -->
<div class="modal-body">
<slot></slot>
</div>
<!-- 底部插槽 -->
<div class="modal-footer">
<slot name="footer">
<button @click="close">取消</button>
<button @click="confirm">确认</button>
</slot>
</div>
</div>
</div>
</template>
<script setup>
defineProps({
visible: Boolean
})
const emit = defineEmits(['update:visible', 'confirm', 'close'])
const close = () => {
emit('update:visible', false)
emit('close')
}
const confirm = () => {
emit('confirm')
close()
}
</script>
<style scoped>
.modal-overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
}
.modal {
background: white;
border-radius: 8px;
min-width: 300px;
max-width: 500px;
}
.modal-header {
padding: 20px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-body {
padding: 20px;
}
.modal-footer {
padding: 20px;
border-top: 1px solid #eee;
text-align: right;
}
.close-btn {
background: none;
border: none;
font-size: 20px;
cursor: pointer;
}
</style>
vue
<!-- 使用模态框 -->
<template>
<button @click="showModal = true">打开模态框</button>
<Modal v-model:visible="showModal" @confirm="handleConfirm">
<!-- 自定义标题 -->
<template #header>
<h3 style="color: red;">⚠️ 警告</h3>
</template>
<!-- 自定义内容 -->
<p>确定要删除这条数据吗?</p>
<p>删除后将无法恢复!</p>
<!-- 自定义底部 -->
<template #footer>
<button @click="showModal = false">取消</button>
<button @click="deleteItem" style="background: red; color: white;">
确认删除
</button>
</template>
</Modal>
</template>
<script setup>
import { ref } from 'vue'
import Modal from './Modal.vue'
const showModal = ref(false)
const handleConfirm = () => {
console.log('确认操作')
}
const deleteItem = () => {
console.log('删除项目')
showModal.value = false
}
</script>
示例2:卡片组件
vue
<!-- Card.vue -->
<template>
<div class="card" :class="{ 'highlight': highlight }">
<!-- 图片插槽 -->
<div v-if="$slots.image" class="card-image">
<slot name="image"></slot>
</div>
<!-- 内容区域 -->
<div class="card-content">
<!-- 标题插槽 -->
<div class="card-title">
<slot name="title">
<h3>默认标题</h3>
</slot>
</div>
<!-- 默认插槽(内容) -->
<div class="card-body">
<slot></slot>
</div>
<!-- 操作区域插槽 -->
<div v-if="$slots.actions" class="card-actions">
<slot name="actions"></slot>
</div>
</div>
</div>
</template>
<script setup>
defineProps({
highlight: {
type: Boolean,
default: false
}
})
</script>
<style scoped>
.card {
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
transition: box-shadow 0.3s;
}
.card.highlight {
border-color: #007bff;
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.2);
}
.card-image img {
width: 100%;
height: 200px;
object-fit: cover;
}
.card-content {
padding: 20px;
}
.card-title {
margin-bottom: 10px;
}
.card-body {
margin-bottom: 15px;
}
.card-actions {
display: flex;
gap: 10px;
margin-top: 15px;
}
</style>
vue
<!-- 使用卡片组件 -->
<template>
<div class="product-list">
<!-- 产品1:完整自定义 -->
<Card highlight>
<template #image>
<img src="/api/placeholder/300/200" alt="产品图片">
</template>
<template #title>
<h3 style="color: #007bff;">🔥 热销产品</h3>
</template>
<p>这款产品是我们的热销款,质量有保证!</p>
<p>价格: <strong>¥299</strong></p>
<template #actions>
<button>加入购物车</button>
<button>立即购买</button>
</template>
</Card>
<!-- 产品2:使用默认 -->
<Card>
<p>普通产品,价格实惠</p>
<p>价格: ¥99</p>
</Card>
</div>
</template>
📊 插槽速查表
| 场景 | 子组件定义 | 父组件使用 | 说明 |
|---|---|---|---|
| 默认插槽 | <slot>默认内容</slot> |
<Child>内容</Child> |
一个组件只能有一个默认插槽 |
| 具名插槽 | <slot name="header"></slot> |
<template #header>内容</template> |
多个具名插槽 |
| 作用域插槽 | <slot :data="data"></slot> |
<Child v-slot="{ data }">{``{ data }}</Child> |
子向父传数据 |
| 动态插槽名 | <slot :name="slotName"></slot> |
<template #[dynamicName]>内容</template> |
动态决定插槽 |
| 嵌套插槽 | 在组件内使用其他组件的插槽 | 正常使用 | 插槽透传 |
🎯 核心要点总结
- 插槽是内容分发机制:允许父组件向子组件传递模板内容
- 三种类型 :
- 默认插槽:
<slot> - 具名插槽:
<slot name="xxx"> - 作用域插槽:
<slot :data="data">
- 默认插槽:
- 使用语法 :
v-slot:name或简写#name- 作用域插槽:
v-slot="{ data }"
- 默认内容 :在
<slot>标签内部可以定义默认内容 - 实际应用:常用于布局组件、UI组件库、可复用组件
简单记忆口诀:
插槽就像预留位,父传内容子显示
默认一个无名槽,具名多个靠名字
作用域槽能传数,灵活使用真强大
通过合理使用插槽,可以创建出高度可定制、可复用的组件,大大提高开发效率。