Vue3 v-slot 详解与示例
v-slot
是 Vue3 中用于处理插槽(Slots)的指令,它提供了更强大和灵活的插槽功能。下面通过详细的示例来展示各种用法。
🎯 基础概念
什么是插槽?
插槽是 Vue 组件的一个强大特性,它允许你在组件中预留位置,让父组件可以插入自定义内容。
📝 基本用法
1. 默认插槽
子组件 BaseLayout.vue
vue
<template>
<div class="container">
<header>
<slot></slot> <!-- 默认插槽 -->
</header>
<main>
<p>这是主内容区域</p>
</main>
<footer>
<p>这是页脚</p>
</footer>
</div>
</template>
父组件使用
vue
<template>
<BaseLayout>
<!-- 插入到默认插槽的内容 -->
<h1>这是页面标题</h1>
<p>这是插入到header插槽的内容</p>
</BaseLayout>
</template>
<script setup>
import BaseLayout from './BaseLayout.vue'
</script>
2. 具名插槽
子组件 Card.vue
vue
<template>
<div class="card">
<div class="card-header">
<slot name="header"></slot> <!-- 具名插槽 -->
</div>
<div class="card-body">
<slot name="body"></slot> <!-- 具名插槽 -->
</div>
<div class="card-footer">
<slot name="footer"></slot> <!-- 具名插槽 -->
</div>
<!-- 默认插槽 -->
<slot></slot>
</div>
</template>
<style scoped>
.card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 16px;
margin: 10px;
}
.card-header {
font-weight: bold;
margin-bottom: 10px;
}
.card-body {
margin-bottom: 10px;
}
.card-footer {
font-size: 0.8em;
color: #666;
}
</style>
父组件使用具名插槽
vue
<template>
<Card>
<!-- 使用 v-slot 指令 -->
<template v-slot:header>
<h2>卡片标题</h2>
</template>
<template v-slot:body>
<p>这是卡片的主要内容区域</p>
<p>可以包含任何HTML内容</p>
</template>
<template v-slot:footer>
<span>卡片底部信息</span>
<button @click="handleClick">操作按钮</button>
</template>
</Card>
</template>
<script setup>
import Card from './Card.vue'
const handleClick = () => {
console.log('按钮被点击了')
}
</script>
🔥 v-slot 语法糖
Vue3 提供了 #
符号作为 v-slot:
的简写形式:
vue
<template>
<Card>
<!-- 简写语法 -->
<template #header>
<h2>简写语法示例</h2>
</template>
<template #body>
<p>使用 # 代替 v-slot:</p>
</template>
<template #footer>
<span>底部内容</span>
</template>
</Card>
</template>
💡 作用域插槽
作用域插槽允许子组件向插槽传递数据,父组件可以访问这些数据。
1. 基础作用域插槽
子组件 DataList.vue
vue
<template>
<div class="data-list">
<ul>
<li v-for="item in items" :key="item.id">
<!-- 向插槽传递数据 -->
<slot :item="item" :index="index"></slot>
</li>
</ul>
</div>
</template>
<script setup>
defineProps({
items: {
type: Array,
default: () => []
}
})
</script>
父组件使用作用域插槽
vue
<template>
<DataList :items="userList">
<!-- 接收子组件传递的数据 -->
<template v-slot:default="slotProps">
<div class="user-item">
<span>{{ slotProps.index + 1 }}. </span>
<strong>{{ slotProps.item.name }}</strong>
<em>({{ slotProps.item.age }} 岁)</em>
</div>
</template>
</DataList>
</template>
<script setup>
import { ref } from 'vue'
import DataList from './DataList.vue'
const userList = ref([
{ id: 1, name: '张三', age: 25 },
{ id: 2, name: '李四', age: 30 },
{ id: 3, name: '王五', age: 28 }
])
</script>
<style scoped>
.user-item {
padding: 8px;
margin: 4px 0;
background-color: #f5f5f5;
border-radius: 4px;
}
</style>
2. 解构作用域插槽参数
vue
<template>
<DataList :items="userList">
<!-- 使用解构语法 -->
<template #default="{ item, index }">
<div class="user-item" :class="{ active: index % 2 === 0 }">
<span>{{ index + 1 }}. </span>
<strong>{{ item.name }}</strong>
<span class="age">{{ item.age }} 岁</span>
</div>
</template>
</DataList>
</template>
🎭 具名作用域插槽
子组件 UserCard.vue
vue
<template>
<div class="user-card">
<!-- 具名作用域插槽 -->
<slot name="avatar" :user="user" :size="avatarSize"></slot>
<div class="user-info">
<slot name="name" :user="user"></slot>
<slot name="details" :user="user"></slot>
</div>
<div class="actions">
<slot name="actions" :user="user" :onEdit="handleEdit"></slot>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const props = defineProps({
user: {
type: Object,
required: true
}
})
const avatarSize = ref('medium')
const handleEdit = () => {
console.log('编辑用户:', props.user.name)
}
</script>
<style scoped>
.user-card {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 16px;
margin: 10px;
display: flex;
align-items: center;
gap: 16px;
}
.user-info {
flex: 1;
}
.actions {
display: flex;
gap: 8px;
}
</style>
父组件使用具名作用域插槽
vue
<template>
<UserCard :user="currentUser">
<!-- 具名作用域插槽 -->
<template #avatar="{ user, size }">
<div class="avatar" :class="size">
<img :src="user.avatar" :alt="user.name" />
</div>
</template>
<template #name="{ user }">
<h3>{{ user.name }}</h3>
</template>
<template #details="{ user }">
<p>邮箱: {{ user.email }}</p>
<p>角色: {{ user.role }}</p>
</template>
<template #actions="{ user, onEdit }">
<button @click="onEdit">编辑</button>
<button @click="deleteUser(user.id)">删除</button>
</template>
</UserCard>
</template>
<script setup>
import { ref } from 'vue'
import UserCard from './UserCard.vue'
const currentUser = ref({
id: 1,
name: '张三',
email: 'zhangsan@example.com',
role: '管理员',
avatar: '/avatars/zhangsan.jpg'
})
const deleteUser = (userId) => {
console.log('删除用户:', userId)
}
</script>
<style scoped>
.avatar {
width: 50px;
height: 50px;
border-radius: 50%;
overflow: hidden;
}
.avatar.medium {
width: 50px;
height: 50px;
}
.avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
</style>
🔄 动态插槽名
vue
<template>
<DynamicComponent>
<!-- 动态插槽名 -->
<template v-slot:[dynamicSlotName]>
<p>这是动态插槽的内容</p>
</template>
<!-- 使用计算属性 -->
<template #[computedSlotName]>
<p>这是计算属性决定的插槽内容</p>
</template>
</DynamicComponent>
</template>
<script setup>
import { ref, computed } from 'vue'
const dynamicSlotName = ref('header')
const slotType = ref('primary')
const computedSlotName = computed(() => {
return `${slotType.value}-content`
})
</script>
📊 高级示例:数据表格组件
子组件 DataTable.vue
vue
<template>
<div class="data-table">
<table>
<thead>
<tr>
<!-- 动态表头 -->
<th v-for="column in columns" :key="column.key">
<slot name="header" :column="column">
{{ column.title }}
</slot>
</th>
</tr>
</thead>
<tbody>
<!-- 动态数据行 -->
<tr v-for="(row, index) in data" :key="row.id">
<td v-for="column in columns" :key="column.key">
<slot
name="cell"
:value="row[column.key]"
:row="row"
:column="column"
:index="index"
>
{{ row[column.key] }}
</slot>
</td>
</tr>
</tbody>
</table>
<!-- 空状态插槽 -->
<div v-if="data.length === 0" class="empty-state">
<slot name="empty">
<p>暂无数据</p>
</slot>
</div>
</div>
</template>
<script setup>
defineProps({
columns: {
type: Array,
default: () => []
},
data: {
type: Array,
default: () => []
}
})
</script>
<style scoped>
.data-table {
width: 100%;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
th {
background-color: #f5f5f5;
}
.empty-state {
text-align: center;
padding: 40px;
color: #666;
}
</style>
父组件使用数据表格
vue
<template>
<DataTable :columns="columns" :data="users">
<!-- 自定义表头 -->
<template #header="{ column }">
<span class="header-cell">
{{ column.title }}
<span v-if="column.sortable" class="sort-icon">↕️</span>
</span>
</template>
<!-- 自定义单元格内容 -->
<template #cell="{ value, row, column }">
<template v-if="column.key === 'avatar'">
<img :src="value" :alt="row.name" class="avatar" />
</template>
<template v-else-if="column.key === 'status'">
<span :class="['status', value]">{{ getStatusText(value) }}</span>
</template>
<template v-else-if="column.key === 'actions'">
<button @click="editUser(row)">编辑</button>
<button @click="deleteUser(row.id)">删除</button>
</template>
<template v-else>
{{ value }}
</template>
</template>
<!-- 自定义空状态 -->
<template #empty>
<div class="custom-empty">
<p>📊 没有找到任何用户数据</p>
<button @click="loadUsers">重新加载</button>
</div>
</template>
</DataTable>
</template>
<script setup>
import { ref } from 'vue'
import DataTable from './DataTable.vue'
const columns = ref([
{ key: 'id', title: 'ID', sortable: true },
{ key: 'name', title: '姓名', sortable: true },
{ key: 'avatar', title: '头像' },
{ key: 'email', title: '邮箱' },
{ key: 'status', title: '状态' },
{ key: 'actions', title: '操作' }
])
const users = ref([
{
id: 1,
name: '张三',
avatar: '/avatars/1.jpg',
email: 'zhangsan@example.com',
status: 'active'
},
{
id: 2,
name: '李四',
avatar: '/avatars/2.jpg',
email: 'lisi@example.com',
status: 'inactive'
}
])
const getStatusText = (status) => {
const statusMap = {
active: '活跃',
inactive: '非活跃'
}
return statusMap[status] || status
}
const editUser = (user) => {
console.log('编辑用户:', user)
}
const deleteUser = (userId) => {
console.log('删除用户:', userId)
}
const loadUsers = () => {
console.log('重新加载用户数据')
}
</script>
<style scoped>
.header-cell {
display: flex;
align-items: center;
gap: 4px;
}
.sort-icon {
font-size: 12px;
}
.avatar {
width: 30px;
height: 30px;
border-radius: 50%;
}
.status {
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
}
.status.active {
background-color: #e8f5e8;
color: #2e7d32;
}
.status.inactive {
background-color: #ffebee;
color: #c62828;
}
.custom-empty {
text-align: center;
padding: 40px;
}
</style>
💡 最佳实践与技巧
1. 提供合理的默认内容
vue
<!-- 子组件 -->
<template>
<div class="notification">
<slot name="icon">
<!-- 默认图标 -->
<span class="default-icon">💡</span>
</slot>
<div class="content">
<slot name="title">
<h3>默认标题</h3>
</slot>
<slot name="message">
<p>默认消息内容</p>
</slot>
</div>
</div>
</template>
2. 使用条件插槽
vue
<template>
<Modal :show="showModal">
<!-- 条件性渲染插槽内容 -->
<template #header v-if="hasCustomHeader">
<CustomHeader :title="modalTitle" />
</template>
<template #body>
<div v-if="isLoading">
<slot name="loading">
<p>加载中...</p>
</slot>
</div>
<div v-else>
<slot name="content"></slot>
</div>
</template>
</Modal>
</template>
3. 插槽性能优化
vue
<template>
<VirtualList :items="largeDataSet">
<!-- 使用 v-memo 优化大量插槽内容 -->
<template #item="{ item, index }">
<div v-memo="[item.id]">
<slot name="item-content" :item="item" :index="index">
{{ item.name }}
</slot>
</div>
</template>
</VirtualList>
</template>
🎯 总结
Vue3 的 v-slot
提供了强大的插槽功能:
特性 | 语法 | 用途 |
---|---|---|
默认插槽 | <slot> |
基本内容插入 |
具名插槽 | v-slot:name 或 #name |
多个插槽区分 |
作用域插槽 | v-slot="props" |
子向父传递数据 |
动态插槽 | v-slot:[dynamicName] |
动态决定插槽名 |
解构语法 | #default="{ prop }" |
简化数据访问 |
关键优势:
-
更好的类型推断(配合 TypeScript)
-
更清晰的语法(特别是简写形式)
-
更强的灵活性(动态插槽、作用域插槽)
-
更好的性能(优化的渲染机制)
掌握这些插槽技术可以让你创建出高度可复用和灵活的组件,大大提升开发效率。