具名插槽与作用域插槽
在上一集中,我们学习了插槽的基础知识,包括匿名插槽和具名插槽。在这一集中,我们将深入学习插槽的高级用法,特别是作用域插槽,它允许父组件在插槽内容中访问子组件的数据,实现更灵活的组件通信和内容定制。
1. 具名插槽的高级用法
1.1 动态插槽名
在Vue 2.6+中,我们可以使用动态指令参数来定义动态的插槽名:
<!-- 父组件 -->
<template>
<div>
<h2>动态插槽名示例</h2>
<!-- 动态插槽名 -->
<ChildComponent>
<template v-slot:[dynamicSlotName]>
<p>这是动态插槽的内容</p>
</template>
<!-- 简写形式 -->
<template #[dynamicSlotName]>
<p>这是动态插槽的内容(简写)</p>
</template>
</ChildComponent>
<!-- 切换插槽名 -->
<button @click="toggleSlotName">切换插槽名</button>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import ChildComponent from './ChildComponent.vue'
const dynamicSlotName = ref('header')
const toggleSlotName = () => {
dynamicSlotName.value = dynamicSlotName.value === 'header' ? 'footer' : 'header'
}
</script>
<!-- 子组件 -->
<template>
<div class="container">
<header>
<slot name="header">默认头部</slot>
</header>
<footer>
<slot name="footer">默认底部</slot>
</footer>
</div>
</template>1.2 插槽的缩写语法
Vue提供了插槽的缩写语法,使用#符号代替v-slot::
<!-- 完整语法 -->
<template v-slot:header>
<h1>自定义头部</h1>
</template>
<!-- 缩写语法 -->
<template #header>
<h1>自定义头部</h1>
</template>2. 作用域插槽
2.1 什么是作用域插槽
作用域插槽是一种特殊的插槽,它允许子组件向插槽内容传递数据。这意味着父组件可以在插槽内容中访问子组件的数据,实现更灵活的内容定制。
2.2 基本用法
在子组件中,我们可以通过v-bind指令(或简写:)向插槽传递数据:
<!-- 子组件 -->
<template>
<div class="list">
<div
v-for="(item, index) in items"
:key="index"
class="list-item"
>
<!-- 向插槽传递数据 -->
<slot :item="item" :index="index" :is-even="index % 2 === 0">
<!-- 默认内容 -->
{{ item }}
</slot>
</div>
</div>
</template>
<script setup>
const props = defineProps({
items: {
type: Array,
default: () => []
}
})
</script>
<!-- 父组件 -->
<template>
<div>
<h2>作用域插槽示例</h2>
<ChildComponent :items="fruits">
<!-- 使用作用域插槽访问子组件数据 -->
<template #default="slotProps">
<div class="fruit-item" :class="{ 'even': slotProps.isEven }">
<span class="index">{{ slotProps.index + 1 }}.</span>
<span class="name">{{ slotProps.item.name }}</span>
<span class="price">¥{{ slotProps.item.price.toFixed(2) }}</span>
</div>
</template>
</ChildComponent>
</div>
</template>
<script setup>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'
const fruits = ref([
{ name: '苹果', price: 5.99 },
{ name: '香蕉', price: 3.99 },
{ name: '橙子', price: 4.99 },
{ name: '葡萄', price: 9.99 }
])
</script>
<style scoped>
.fruit-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px;
border-bottom: 1px solid #eee;
}
.fruit-item.even {
background-color: #f8fafc;
}
.index {
font-weight: bold;
color: #666;
width: 20px;
}
.name {
flex: 1;
}
.price {
color: #ef4444;
font-weight: bold;
}
</style>2.3 解构插槽 props
我们可以使用ES6的解构语法来简化插槽props的使用:
<!-- 父组件 -->
<template>
<ChildComponent :items="fruits">
<!-- 解构插槽props -->
<template #default="{ item, index, isEven }">
<div class="fruit-item" :class="{ 'even': isEven }">
<span class="index">{{ index + 1 }}.</span>
<span class="name">{{ item.name }}</span>
<span class="price">¥{{ item.price.toFixed(2) }}</span>
</div>
</template>
</ChildComponent>
</template>2.4 具名作用域插槽
作用域插槽也可以是具名插槽:
<!-- 子组件 -->
<template>
<div class="card">
<div class="card-header">
<!-- 具名作用域插槽 -->
<slot name="header" :title="defaultTitle">{{ defaultTitle }}</slot>
</div>
<div class="card-body">
<!-- 匿名作用域插槽 -->
<slot :content="defaultContent">{{ defaultContent }}</slot>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const defaultTitle = ref('默认标题')
const defaultContent = ref('默认内容')
</script>
<!-- 父组件 -->
<template>
<ChildComponent>
<!-- 具名作用域插槽 -->
<template #header="{ title }">
<h3>{{ title }} - 自定义头部</h3>
</template>
<!-- 匿名作用域插槽 -->
<template #default="{ content }">
<p>{{ content }} - 自定义内容</p>
</template>
</ChildComponent>
</template>3. 独占默认插槽的缩写
当一个组件只有一个默认插槽时,我们可以使用更简洁的语法:
<!-- 完整语法 -->
<ChildComponent>
<template #default="slotProps">
<p>{{ slotProps.message }}</p>
</template>
</ChildComponent>
<!-- 缩写语法 -->
<ChildComponent #default="slotProps">
<p>{{ slotProps.message }}</p>
</ChildComponent>
<!-- 进一步缩写 -->
<ChildComponent v-slot="slotProps">
<p>{{ slotProps.message }}</p>
</ChildComponent>4. 组合式API中的作用域插槽
在组合式API中,我们可以使用useSlots函数来访问作用域插槽:
<template>
<div class="container">
<slot name="header" :data="headerData">默认头部</slot>
<slot :data="bodyData">默认内容</slot>
</div>
</template>
<script setup>
import { ref, useSlots, onMounted } from 'vue'
const headerData = ref('头部数据')
const bodyData = ref('内容数据')
const slots = useSlots()
onMounted(() => {
// 检查是否提供了header插槽
if (slots.header) {
console.log('提供了header插槽')
}
// 检查是否提供了默认插槽
if (slots.default) {
console.log('提供了默认插槽')
}
})
</script>5. 动态插槽内容
我们可以使用v-if、v-for等指令来动态生成插槽内容:
<template>
<ChildComponent>
<template #item="{ item, index }">
<div v-if="item.isActive" class="active-item">
<span>{{ index + 1 }}. {{ item.name }}</span>
<button @click="deactivateItem(index)">停用</button>
</div>
<div v-else class="inactive-item">
<span>{{ index + 1 }}. {{ item.name }}</span>
<button @click="activateItem(index)">激活</button>
</div>
</template>
</ChildComponent>
</template>6. 完整示例:自定义表格组件
<!-- CustomTable.vue 子组件 -->
<template>
<div class="custom-table">
<!-- 表格头部 -->
<div class="table-header">
<div
v-for="(column, index) in columns"
:key="index"
class="table-header-cell"
:style="{ width: column.width || 'auto' }"
>
<slot name="header-cell" :column="column" :index="index">
{{ column.label }}
</slot>
</div>
</div>
<!-- 表格主体 -->
<div class="table-body">
<div
v-for="(row, rowIndex) in data"
:key="rowIndex"
class="table-row"
:class="{ 'row-even': rowIndex % 2 === 0 }"
>
<div
v-for="(column, colIndex) in columns"
:key="colIndex"
class="table-cell"
:style="{ width: column.width || 'auto' }"
>
<slot name="cell" :row="row" :column="column" :row-index="rowIndex" :col-index="colIndex">
{{ row[column.prop] }}
</slot>
</div>
</div>
<!-- 空表格 -->
<div v-if="data.length === 0" class="empty-table">
<slot name="empty">
<p>表格为空</p>
</slot>
</div>
</div>
<!-- 表格底部 -->
<div class="table-footer">
<slot name="footer">
<p>共 {{ data.length }} 行数据</p>
</slot>
</div>
</div>
</template>
<script setup>
const props = defineProps({
// 表格列配置
columns: {
type: Array,
required: true
},
// 表格数据
data: {
type: Array,
default: () => []
}
})
</script>
<style scoped>
.custom-table {
border: 1px solid #e2e8f0;
border-radius: 8px;
overflow: hidden;
max-width: 800px;
margin: 0 auto;
}
.table-header {
display: flex;
background-color: #f8fafc;
border-bottom: 1px solid #e2e8f0;
}
.table-header-cell {
padding: 12px 16px;
font-weight: 600;
color: #1e293b;
border-right: 1px solid #e2e8f0;
}
.table-header-cell:last-child {
border-right: none;
}
.table-body {
max-height: 400px;
overflow-y: auto;
}
.table-row {
display: flex;
border-bottom: 1px solid #f1f5f9;
}
.table-row:last-child {
border-bottom: none;
}
.table-row.row-even {
background-color: #fafafa;
}
.table-cell {
padding: 12px 16px;
border-right: 1px solid #f1f5f9;
}
.table-cell:last-child {
border-right: none;
}
.empty-table {
padding: 32px;
text-align: center;
color: #64748b;
}
.table-footer {
padding: 12px 16px;
background-color: #f8fafc;
border-top: 1px solid #e2e8f0;
text-align: right;
color: #64748b;
}
</style>
<!-- 父组件 -->
<template>
<div class="table-demo">
<h2>自定义表格组件示例</h2>
<CustomTable :columns="columns" :data="users">
<!-- 自定义头部单元格 -->
<template #header-cell="{ column }">
<div class="custom-header-cell">
<span>{{ column.label }}</span>
<button v-if="column.sortable" @click="toggleSort(column.prop)">
{{ sortColumn === column.prop ? (sortOrder === 'asc' ? '↑' : '↓') : '↕' }}
</button>
</div>
</template>
<!-- 自定义内容单元格 -->
<template #cell="{ row, column }">
<div v-if="column.prop === 'name'" class="name-cell">
<span class="avatar">{{ row.name.charAt(0) }}</span>
<span>{{ row.name }}</span>
</div>
<div v-else-if="column.prop === 'status'" class="status-cell">
<span :class="['status-badge', `status-${row.status}`]">
{{ row.status === 'active' ? '活跃' : row.status === 'inactive' ? '停用' : ' pending' }}
</span>
</div>
<div v-else-if="column.prop === 'actions'" class="actions-cell">
<button @click="editUser(row.id)" class="btn-edit">编辑</button>
<button @click="deleteUser(row.id)" class="btn-delete">删除</button>
</div>
<div v-else>
{{ row[column.prop] }}
</div>
</template>
<!-- 自定义空表格内容 -->
<template #empty>
<div class="custom-empty">
<p>暂无用户数据</p>
<button @click="addUser">添加用户</button>
</div>
</template>
<!-- 自定义表格底部 -->
<template #footer>
<div class="custom-footer">
<p>共 {{ users.length }} 位用户</p>
<button @click="refreshData">刷新数据</button>
</div>
</template>
</CustomTable>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import CustomTable from './CustomTable.vue'
// 表格列配置
const columns = ref([
{ prop: 'name', label: '姓名', width: '150px', sortable: true },
{ prop: 'email', label: '邮箱', width: '250px' },
{ prop: 'status', label: '状态', width: '100px' },
{ prop: 'createdAt', label: '创建时间', width: '180px' },
{ prop: 'actions', label: '操作', width: '120px' }
])
// 表格数据
const users = ref([
{ id: 1, name: '张三', email: 'zhangsan@example.com', status: 'active', createdAt: '2023-01-01' },
{ id: 2, name: '李四', email: 'lisi@example.com', status: 'inactive', createdAt: '2023-02-01' },
{ id: 3, name: '王五', email: 'wangwu@example.com', status: 'active', createdAt: '2023-03-01' },
{ id: 4, name: '赵六', email: 'zhaoliu@example.com', status: 'pending', createdAt: '2023-04-01' }
])
// 排序状态
const sortColumn = ref('')
const sortOrder = ref('asc')
// 切换排序
const toggleSort = (column) => {
if (sortColumn.value === column) {
sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc'
} else {
sortColumn.value = column
sortOrder.value = 'asc'
}
// 执行排序
users.value.sort((a, b) => {
if (a[column] < b[column]) {
return sortOrder.value === 'asc' ? -1 : 1
}
if (a[column] > b[column]) {
return sortOrder.value === 'asc' ? 1 : -1
}
return 0
})
}
// 编辑用户
const editUser = (id) => {
alert(`编辑用户 ${id}`)
}
// 删除用户
const deleteUser = (id) => {
users.value = users.value.filter(user => user.id !== id)
}
// 添加用户
const addUser = () => {
const newUser = {
id: users.value.length + 1,
name: `用户${users.value.length + 1}`,
email: `user${users.value.length + 1}@example.com`,
status: 'active',
createdAt: new Date().toISOString().split('T')[0]
}
users.value.push(newUser)
}
// 刷新数据
const refreshData = () => {
alert('刷新数据')
}
</script>
<style scoped>
.table-demo {
padding: 20px;
}
.custom-header-cell {
display: flex;
align-items: center;
justify-content: space-between;
}
.name-cell {
display: flex;
align-items: center;
gap: 8px;
}
.avatar {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background-color: #3b82f6;
color: white;
border-radius: 50%;
font-weight: 600;
font-size: 14px;
}
.status-cell {
display: flex;
align-items: center;
}
.status-badge {
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
}
.status-active {
background-color: #d1fae5;
color: #065f46;
}
.status-inactive {
background-color: #fee2e2;
color: #991b1b;
}
.status-pending {
background-color: #fef3c7;
color: #92400e;
}
.actions-cell {
display: flex;
gap: 8px;
}
.btn-edit,
.btn-delete {
padding: 4px 8px;
border: none;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
}
.btn-edit {
background-color: #3b82f6;
color: white;
}
.btn-delete {
background-color: #ef4444;
color: white;
}
.custom-empty {
text-align: center;
}
.custom-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
button {
padding: 6px 12px;
background-color: #3b82f6;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
button:hover {
background-color: #2563eb;
}
</style>7. 作用域插槽的最佳实践
7.1 明确数据传递
在设计作用域插槽时,明确要传递给父组件的数据,避免传递不必要的数据,提高组件的性能和易用性。
7.2 使用描述性的prop名称
为作用域插槽的props使用描述性的名称,提高代码的可读性和可维护性。
7.3 提供合理的默认内容
为作用域插槽提供合理的默认内容,当父组件没有提供自定义内容时,组件仍然可以正常显示。
7.4 结合v-for使用
作用域插槽经常与v-for结合使用,用于自定义列表项、表格行等重复元素的渲染。
7.5 避免过度复杂化
虽然作用域插槽非常灵活,但过度使用会增加组件的复杂性。对于简单的组件,考虑使用Props和事件的组合,而不是作用域插槽。
8. 常见问题与解决方案
8.1 为什么无法访问作用域插槽的数据?
- 检查是否在子组件中正确绑定了数据
- 检查父组件中是否正确使用了作用域插槽语法
- 检查插槽名称是否匹配
8.2 如何在作用域插槽中使用v-model?
可以在作用域插槽中使用v-model,但需要注意双向绑定的数据流:
<template>
<ChildComponent>
<template #item="{ item }">
<input v-model="item.name" type="text" />
<!-- 注意:这会直接修改子组件的数据,违反单向数据流原则 -->
</template>
</ChildComponent>
</template>更好的做法是通过事件通知父组件更新数据:
<template>
<ChildComponent>
<template #item="{ item, updateItem }">
<input
:value="item.name"
@input="updateItem({ ...item, name: $event.target.value })"
type="text"
/>
</template>
</ChildComponent>
</template>8.3 如何在作用域插槽中使用计算属性?
可以在父组件中定义计算属性,然后在作用域插槽中使用:
<template>
<ChildComponent>
<template #item="{ item }">
<div :class="{ 'highlight': isHighlighted(item) }">
{{ item.name }}
</div>
</template>
</ChildComponent>
</template>
<script setup>
import { ref, computed } from 'vue'
const isHighlighted = (item) => {
return item.score > 90
}
</script>9. 总结
在这一集中,我们深入学习了Vue中的插槽高级用法,包括具名插槽和作用域插槽。
具名插槽允许我们在子组件中定义多个插槽,父组件可以根据名称插入不同的内容,实现复杂的布局。作用域插槽则允许子组件向插槽内容传递数据,父组件可以在插槽内容中访问这些数据,实现更灵活的内容定制。
作用域插槽的主要特点包括:
- 允许子组件向插槽内容传递数据
- 支持解构语法,简化代码
- 可以是具名插槽
- 与v-for结合使用,实现自定义列表项
- 与组合式API良好兼容
在实际开发中,我们应该根据组件的复杂度和需求,合理使用插槽类型:
- 对于简单的内容分发,使用匿名插槽
- 对于复杂的布局,使用具名插槽
- 对于需要访问子组件数据的情况,使用作用域插槽
通过合理使用插槽,我们可以创建出更加灵活、易用和可维护的Vue组件。
10. 练习题
创建一个自定义表格组件,支持以下功能:
- 动态列配置
- 作用域插槽自定义列内容
- 排序功能
- 分页功能
- 筛选功能
创建一个自定义卡片组件,支持以下功能:
- 具名插槽(header、body、footer)
- 作用域插槽传递卡片数据
- 支持不同的卡片样式
- 支持卡片动画效果
创建一个自定义树形组件,支持以下功能:
- 递归渲染树形结构
- 作用域插槽自定义节点内容
- 节点展开/折叠功能
- 节点选择功能
- 搜索过滤功能
通过这些练习,你将更加熟悉Vue中的具名插槽和作用域插槽,能够创建出更加灵活和强大的组件。