Vue 3 插槽完全指南:构建灵活组件的终极武器
·
Vue 3 插槽完全指南:构建灵活组件的终极武器
文章目录
1. 插槽基础概念
1.1 什么是插槽?
插槽是 Vue 组件的一种内容分发机制,允许父组件向子组件传递模板内容。
1.2 基本语法
<!-- 子组件 ChildComponent.vue -->
<template>
<div class="child">
<h2>子组件标题</h2>
<slot></slot>
</div>
</template>
<!-- 父组件使用 -->
<template>
<ChildComponent>
<p>这是插入到子组件中的内容</p>
</ChildComponent>
</template>
2. 默认插槽
2.1 基本默认插槽
<!-- 子组件 BaseCard.vue -->
<template>
<div class="card">
<div class="card-header">
<slot>默认内容</slot>
</div>
</div>
</template>
<!-- 父组件使用 -->
<template>
<!-- 使用自定义内容 -->
<BaseCard>
<h3>自定义标题</h3>
</BaseCard>
<!-- 使用默认内容 -->
<BaseCard />
</template>
2.2 多个插槽内容
<!-- 父组件 -->
<template>
<BaseCard>
<p>第一段内容</p>
<p>第二段内容</p>
<p>第三段内容</p>
</BaseCard>
</template>
<!-- 渲染结果 -->
<div class="card">
<div class="card-header">
<p>第一段内容</p>
<p>第二段内容</p>
<p>第三段内容</p>
</div>
</div>
3. 具名插槽
3.1 基本具名插槽
<!-- 子组件 Layout.vue -->
<template>
<div class="layout">
<header class="header">
<slot name="header"></slot>
</header>
<main class="main">
<slot></slot> <!-- 默认插槽 -->
</main>
<footer class="footer">
<slot name="footer"></slot>
</footer>
</div>
</template>
<!-- 父组件使用 -->
<template>
<Layout>
<template #header>
<h1>页面标题</h1>
</template>
<!-- 默认插槽内容 -->
<p>这是主要内容...</p>
<p>更多内容...</p>
<template #footer>
<p>版权信息 © 2024</p>
</template>
</Layout>
</template>
3.2 具名插槽的语法糖
<!-- 三种等价写法 -->
<template #header>...</template>
<template v-slot:header>...</template>
<slot name="header">...</slot>
4. 作用域插槽
4.1 基本作用域插槽
<!-- 子组件 UserList.vue -->
<template>
<ul>
<li v-for="user in users" :key="user.id">
<slot :user="user" :index="index"></slot>
</li>
</ul>
</template>
<script setup>
import { ref } from 'vue'
const users = ref([
{ id: 1, name: 'Alice', age: 25 },
{ id: 2, name: 'Bob', age: 30 }
])
</script>
<!-- 父组件使用 -->
<template>
<UserList>
<template #default="{ user, index }">
<span>{{ index + 1 }}. {{ user.name }} - {{ user.age }}岁</span>
</template>
</UserList>
</template>
4.2 具名作用域插槽
<!-- 子组件 DataTable.vue -->
<template>
<table>
<thead>
<tr>
<slot name="header" :columns="columns"></slot>
</tr>
</thead>
<tbody>
<tr v-for="item in data" :key="item.id">
<slot name="body" :item="item" :index="index"></slot>
</tr>
</tbody>
</table>
</template>
<script setup>
const columns = ['姓名', '年龄', '操作']
const data = [
{ id: 1, name: 'Alice', age: 25 },
{ id: 2, name: 'Bob', age: 30 }
]
</script>
<!-- 父组件使用 -->
<template>
<DataTable>
<template #header="{ columns }">
<th v-for="col in columns" :key="col">{{ col }}</th>
</template>
<template #body="{ item, index }">
<td>{{ item.name }}</td>
<td>{{ item.age }}</td>
<td>
<button @click="editItem(item)">编辑</button>
</td>
</template>
</DataTable>
</template>
5. 动态插槽名
5.1 动态插槽
<!-- 父组件 -->
<template>
<DynamicSlotComponent>
<template #[dynamicSlotName]>
<p>动态插槽内容</p>
</template>
<template v-slot:[anotherSlotName]>
<p>另一个动态插槽</p>
</template>
</DynamicSlotComponent>
</template>
<script setup>
import { ref } from 'vue'
const dynamicSlotName = ref('header')
const anotherSlotName = ref('footer')
</script>
6. 插槽高级用法
6.1 条件插槽
<!-- 子组件 ConditionalSlot.vue -->
<template>
<div>
<slot name="content" v-if="showContent"></slot>
<slot name="fallback" v-else></slot>
</div>
</template>
<script setup>
defineProps({
showContent: Boolean
})
</script>
<!-- 父组件使用 -->
<template>
<ConditionalSlot :show-content="isContentVisible">
<template #content>
<p>主要内容</p>
</template>
<template #fallback>
<p>备用内容</p>
</template>
</ConditionalSlot>
</template>
6.2 插槽传递
<!-- 中间组件 Wrapper.vue -->
<template>
<ChildComponent>
<!-- 将插槽传递给子组件 -->
<template v-for="(_, name) in $slots" #[name]="slotProps">
<slot :name="name" v-bind="slotProps"></slot>
</template>
</ChildComponent>
</template>
7. 使用 <script setup> 的插槽
7.1 插槽类型定义
<!-- 子组件 TypedSlot.vue -->
<template>
<div>
<slot name="item" :data="itemData" :index="0"></slot>
<slot :user="currentUser"></slot>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
interface User {
name: string
age: number
}
interface ItemData {
id: number
title: string
}
// 组件数据
const itemData = ref<ItemData>({ id: 1, title: '示例' })
const currentUser = ref<User>({ name: 'John', age: 25 })
// 插槽类型定义(用于 TypeScript)
defineSlots<{
// 具名插槽
item: (props: { data: ItemData; index: number }) => any
// 默认插槽
default: (props: { user: User }) => any
}>()
</script>
7.2 使用 useSlots()
<!-- 子组件 SlotInspector.vue -->
<template>
<div>
<div v-if="hasHeaderSlot" class="header">
<slot name="header"></slot>
</div>
<div class="content">
<slot></slot>
</div>
<div v-if="hasFooterSlot" class="footer">
<slot name="footer"></slot>
</div>
</div>
</template>
<script setup>
import { useSlots, computed } from 'vue'
const slots = useSlots()
// 检查插槽是否存在
const hasHeaderSlot = computed(() => !!slots.header)
const hasFooterSlot = computed(() => !!slots.footer)
const hasDefaultSlot = computed(() => !!slots.default)
</script>
8. 实战案例
8.1 可复用的模态框组件
<!-- Modal.vue -->
<template>
<div class="modal" v-if="visible">
<div class="modal-content">
<div class="modal-header">
<slot name="header">
<h3>{{ title }}</h3>
</slot>
<button @click="$emit('close')" class="close-btn">×</button>
</div>
<div class="modal-body">
<slot></slot>
</div>
<div class="modal-footer">
<slot name="footer">
<button @click="$emit('close')">关闭</button>
</slot>
</div>
</div>
</div>
</template>
<script setup>
defineProps({
visible: Boolean,
title: {
type: String,
default: '默认标题'
}
})
defineEmits(['close'])
</script>
<style scoped>
.modal {
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
background: rgba(0,0,0,0.5);
display: flex;
justify-content: center;
align-items: center;
}
.modal-content {
background: white;
padding: 20px;
border-radius: 8px;
min-width: 400px;
}
.modal-header {
display: flex;
justify-content: between;
align-items: center;
margin-bottom: 15px;
}
.close-btn {
background: none;
border: none;
font-size: 20px;
cursor: pointer;
}
</style>
<!-- 父组件使用 -->
<template>
<button @click="showModal = true">打开模态框</button>
<Modal v-model:visible="showModal" title="用户信息">
<template #header>
<h3 style="color: blue;">自定义标题</h3>
</template>
<p>这是模态框的主要内容...</p>
<p>可以放置任何内容</p>
<template #footer>
<button @click="showModal = false">取消</button>
<button @click="confirm">确认</button>
</template>
</Modal>
</template>
<script setup>
import { ref } from 'vue'
const showModal = ref(false)
const confirm = () => {
console.log('确认操作')
showModal.value = false
}
</script>
8.2 数据列表组件
<!-- DataList.vue -->
<template>
<div class="data-list">
<div class="list-header">
<slot name="header" :total="data.length" :filtered="filteredData.length">
共 {{ filteredData.length }} 条数据
</slot>
</div>
<div class="list-items">
<div
v-for="(item, index) in filteredData"
:key="item.id"
class="list-item"
>
<slot
name="item"
:item="item"
:index="index"
:is-selected="selectedItem?.id === item.id"
>
<div>{{ item.name }}</div>
</slot>
</div>
</div>
<div class="list-footer">
<slot name="footer" :selected="selectedItem"></slot>
</div>
</div>
</template>
<script setup>
import { computed, ref } from 'vue'
const props = defineProps({
data: {
type: Array,
default: () => []
},
filter: {
type: String,
default: ''
}
})
const selectedItem = ref(null)
const filteredData = computed(() => {
if (!props.filter) return props.data
return props.data.filter(item =>
item.name.toLowerCase().includes(props.filter.toLowerCase())
)
})
const selectItem = (item) => {
selectedItem.value = item
}
// 暴露方法给父组件
defineExpose({
selectItem,
selectedItem
})
</script>
<!-- 父组件使用 -->
<template>
<DataList :data="users" :filter="searchText">
<template #header="{ total, filtered }">
<div style="display: flex; justify-content: space-between;">
<span>用户列表 ({{ filtered }}/{{ total }})</span>
<input v-model="searchText" placeholder="搜索用户..." />
</div>
</template>
<template #item="{ item, index, isSelected }">
<div
:class="['user-item', { selected: isSelected }]"
@click="selectUser(item)"
>
<div class="user-avatar">{{ item.name.charAt(0) }}</div>
<div class="user-info">
<div class="user-name">{{ item.name }}</div>
<div class="user-email">{{ item.email }}</div>
</div>
<div class="user-actions">
<button @click.stop="editUser(item)">编辑</button>
</div>
</div>
</template>
<template #footer="{ selected }">
<div v-if="selected" class="selected-info">
已选择: {{ selected.name }}
</div>
</template>
</DataList>
</template>
<script setup>
import { ref } from 'vue'
const searchText = ref('')
const users = ref([
{ id: 1, name: 'Alice', email: 'alice@example.com' },
{ id: 2, name: 'Bob', email: 'bob@example.com' },
{ id: 3, name: 'Charlie', email: 'charlie@example.com' }
])
const selectUser = (user) => {
console.log('选择用户:', user)
}
const editUser = (user) => {
console.log('编辑用户:', user)
}
</script>
<style scoped>
.user-item {
display: flex;
align-items: center;
padding: 10px;
border: 1px solid #ddd;
margin: 5px 0;
cursor: pointer;
}
.user-item.selected {
background-color: #e3f2fd;
border-color: #2196f3;
}
.user-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: #2196f3;
color: white;
display: flex;
align-items: center;
justify-content: center;
margin-right: 10px;
}
.user-info {
flex: 1;
}
.user-name {
font-weight: bold;
}
.user-email {
color: #666;
font-size: 0.9em;
}
</style>
9. 最佳实践
9.1 提供合理的默认内容
<template>
<div class="component">
<slot name="title">
<h2>默认标题</h2>
</slot>
<slot>
<p>请提供内容</p>
</slot>
</div>
</template>
9.2 使用作用域插槽提供数据
<template>
<slot :data="componentData" :methods="{ addItem, removeItem }"></slot>
</template>
9.3 类型安全的插槽(TypeScript)
<script setup lang="ts">
interface SlotProps {
item: User
index: number
isActive: boolean
}
defineSlots<{
default: (props: SlotProps) => any
header: (props: { title: string }) => any
}>()
</script>
10. 常见问题与解决方案
10.1 插槽内容更新问题
<!-- 使用 key 强制重新渲染 -->
<slot :key="item.id"></slot>
10.2 条件插槽渲染
<template>
<div v-if="$slots.default">
<slot></slot>
</div>
<div v-else>
<p>没有内容</p>
</div>
</template>
更多推荐
所有评论(0)