Vue3 v-slot 详解与示例
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)
更清晰的语法(特别是简写形式)
更强的灵活性(动态插槽、作用域插槽)
更好的性能(优化的渲染机制)
掌握这些插槽技术可以让你创建出高度可复用和灵活的组件,大大提升开发效率。