插槽是 Vue 组件中一个非常核心的概念,它允许你以一种灵活的方式将内容"插入"到子组件的指定位置,极大地提高了组件的复用性和灵TA性。插槽允许组件只负责渲染一个"框架"(比如边框、阴影),而把"内容"的决定权交给使用它的父组件。
1.默认插槽
最简单的插槽,子组件中只有一个未命名的 <slot> 出口。
子组件
ts
<template>
<div class="card">
<h3>卡片标题</h3>
<slot></slot> </div>
</template>
<style scoped>
.card {
border: 1px solid #ccc;
border-radius: 8px;
padding: 16px;
max-width: 300px;
}
</style>
父组件
ts
<template>
<BaseCard>
<p>这是一父组件</p>
<img src="./assets/logo.png" alt="Vue Logo" style="width: 100px;">
</BaseCard>
</template>
<script setup lang="ts">
import BaseCard from './components/BaseCard.vue';
</script>
2.具名插槽
当子组件需要多个"坑位"时(例如,一个用于头部,一个用于底部),就需要使用具名插槽。
子组件:使用
name属性来区分不同的插槽。
ts
<template>
<div class="modal">
<header class="modal-header">
<slot name="header"></slot> </header>
<main class="modal-body">
<slot></slot> </main>
<footer class="modal-footer">
<slot name="footer"></slot> </footer>
</div>
</template>
<style scoped>
.modal { background: #fff; border: 1px solid #ddd; }
.modal-header, .modal-footer { padding: 10px; background: #f4f4f4; }
.modal-body { padding: 20px; }
</style>
父组件:使用
<template>标签和v-slot指令(或其简写#)来指定要填充的插槽。
ts
<template>
<ModalLayout>
<template v-slot = "header">
<h2>这是一个模态框标题</h2>
</template>
<p>这是模态框的主要内容...</p>
<template #footer>
<button>取消</button>
<button>确认</button>
</template>
</ModalLayout>
</template>
<script setup lang="ts">
import ModalLayout from './components/ModalLayout.vue';
</script>
3.作用域插槽
这是插槽最强大的功能。它允许子组件向父组件的插槽内容传递数据。这在处理列表渲染时非常有用,子组件负责数据迭代,而父组件负责定义每一项的渲染样式。
子组件:子组件通过在
<slot>标签上绑定属性,来将数据"暴露"给父组件。
ts
<template>
<div class="user-list">
<p>用户列表:</p>
<ul>
<li v-for="user in users" :key="user.id">
<slot :user="user" :isAdmin="user.name === 'Alice'"></slot>
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
interface User {
id: number;
name: string;
age: number;
}
const users = ref<User[]>([
{ id: 1, name: 'Alice', age: 30 },
{ id: 2, name: 'Bob', age: 25 },
]);
</script>
父组件:父组件通过
v-slot(或#) 接收子组件传递的数据,并且可以立即为这些数据添加 TypeScript 类型。
ts
<template>
<UserList>
<template #default="{ user, isAdmin }: { user: User, isAdmin: boolean }">
<span>
{{ user.name }} ({{ user.age }}岁)
</span>
<span v-if="isAdmin" style="color: red; margin-left: 10px;">[管理员]</span>
</template>
</UserList>
</template>
<script setup lang="ts">
import UserList from './components/UserList.vue';
// 我们可以在父组件中也定义这个类型,以便复用
interface User {
id: number;
name: string;
age: number;
}
</script>
4.自定义插槽(Vue 3.3+)
在 Vue 3.3 及更高版本中, <script setup> 提供了 defineSlots 宏,这是在子组件中为插槽提供类型 的官方方式。这极大地改善了开发体验,父组件不再需要手动声明类型,因为 TS 可以自动从子组件推断它们。
子组件:使用
defineSlots来声明插槽及其期望的props类型。
ts
<template>
<div class="list">
<ul>
<li v-for="(item, index) in items" :key="item.id">
<slot :item="item" :index="index"></slot>
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
// 1. 定义数据和类型
interface Item {
id: string;
text: string;
}
const items = ref<Item[]>([
{ id: 'a', text: '第一项' },
{ id: 'b', text: '第二项' },
]);
// 2. (重点) 使用 defineSlots
// 这是一个宏,无需导入
// 它定义了 'default' 插槽会接收一个对象,该对象包含一个 'item' 属性 (类型为 Item)
// 和一个 'index' 属性 (类型为 number)
defineSlots<{
default(props: { item: Item; index: number }): any;
// 如果有具名插槽,也可以在这里定义,比如:
// header(props: { title: string }): any;
}>();
</script>
父组件:父组件的
slotProps(或解构的变量) 会被自动推断出正确的类型
ts
<template>
<TypedList>
<template #default="{ item, index }">
<strong>{{ index + 1 }}.</strong> {{ item.text.toUpperCase() }}
</template>
</TypedList>
</template>
<script setup lang="ts">
import TypedList from './components/TypedList.vue';
</script>
5.总结
-
布局组件 (
Layout.vue):- 场景: 定义网站的通用布局,如侧边栏、顶部导航和内容区域。
- 用法 : 使用
header,sidebar,main等具名插槽,让不同页面填充自己的内容。
-
可复用 UI 元素 (
Modal.vue,Card.vue,Dropdown.vue):- 场景: 封装通用的交互和样式,但允许内容高度自定义。
- 用法 :
Modal组件提供header(标题),body(内容),footer(按钮) 插槽。
-
列表渲染器 (
DataList.vue,ProductGrid.vue):- 场景 : 组件负责获取和迭代数据(如 API 请求、分页),但把如何渲染每一项的控制权交给父组件。
- 用法 : (核心) 使用作用域插槽 ,将
item(当前项数据) 传递给父组件。这是最灵活的模式。
-
提供者组件 (
Toggle.vue,MouseTracker.vue):- 场景 : 组件管理某个状态(如
isOn)或逻辑(如鼠标位置),并通过作用域插槽将这些状态暴露出去,让父组件来决定如何渲染。 - 用法 : 子组件
<slot :isOn="isOn" :toggle="toggleFunction"></slot>。
- 场景 : 组件管理某个状态(如