在 Vue3 组件开发中,插槽(Slot)是实现组件复用与灵活扩展的核心技术之一,也是组件通信的重要方式(父子组件间传递内容)。很多新手开发者在使用插槽时,容易混淆默认插槽、具名插槽、作用域插槽的用法,不清楚什么时候该用哪种插槽,导致组件封装不够灵活、代码冗余。
一、插槽核心概念:为什么需要插槽?
插槽的本质是「组件预留的内容分发出口」,核心作用是:让父组件可以向子组件中插入自定义内容,实现组件的灵活复用,避免组件硬编码导致的扩展性差问题。
举个简单例子:我们封装一个「卡片组件」,卡片的标题、内容、底部按钮可能在不同场景下有不同样式和内容,如果直接在子组件中写死内容,这个组件就只能用在一个场景;而通过插槽预留出口,父组件可以根据自己的需求,向卡片中插入任意内容,实现"一套组件,多场景复用"。
Vue3 中的插槽分为三大类,优先级从基础到进阶:默认插槽 → 具名插槽 → 作用域插槽,下面逐一讲解实战用法。
二、基础用法:默认插槽(匿名插槽)
1. 核心特点
默认插槽是最基础的插槽类型,无需给插槽命名,子组件中用 标签预留出口,父组件中直接在子组件标签内写入内容,即可自动分发到默认插槽中。
适用场景:子组件只有一个可自定义区域,比如卡片的内容区、弹窗的主体内容等。
2. 实战案例(Vue3 + TS)
需求:封装一个基础卡片组件,父组件可自定义卡片内容,卡片标题固定为"默认插槽示例"。
(1)子组件:CardDefault.vue(预留默认插槽)
vue
<script setup lang="ts">
// 子组件:预留默认插槽,无额外逻辑,仅提供内容分发出口
defineProps<{
// 可添加组件自身的props,与插槽不冲突
title?: string
}>()
</script>
<template>
<div class="card" style="border: 1px solid #eee; padding: 20px; border-radius: 8px; max-width: 400px; margin: 0 auto;">
<h3 style="color: #333; margin: 0 0 15px 0;">{{ title || '默认插槽示例' }}</h3>
<!-- 默认插槽:未命名,父组件内容会自动插入到这里 -->
<slot>
<!-- 插槽默认内容:当父组件未插入内容时,显示该内容 -->
请插入卡片内容(父组件未提供内容时显示)
</slot>
</div>
</template>
(2)父组件:使用默认插槽
vue
<script setup lang="ts">
import CardDefault from '@/components/CardDefault.vue'
</script>
<template>
<div style="padding: 20px;">
<h2 style="text-align: center; margin-bottom: 30px;">默认插槽实战</h2>
<!-- 用法1:插入简单文本 -->
<CardDefault title="文本内容示例" />
<div style="height: 20px;"></div>
<!-- 用法2:插入复杂内容(标签、组件等) -->
<CardDefault title="复杂内容示例">
<div style="line-height: 1.6;">
<p>这是父组件插入的复杂内容</p>
<p>可以包含多个标签、样式,甚至其他组件</p>
<button style="padding: 6px 12px; margin-top: 10px; background: #42b983; color: white; border: none; border-radius: 4px;">
父组件插入的按钮
</button>
</div>
</CardDefault>
</div>
</template>
3. 关键注意点
- 默认插槽只能有一个,子组件中多个
<slot>未命名时,父组件内容会同时插入到所有未命名插槽中(无意义,不推荐)。 - 插槽默认内容:
<slot>标签内部的内容,只有当父组件未向插槽插入任何内容时才会显示,用于兜底提示。 - 父组件插入的内容,作用域属于父组件(可访问父组件的数据、方法,无法直接访问子组件数据)。
三、进阶用法:具名插槽(命名插槽)
1. 核心特点
当子组件有多个可自定义区域 时,默认插槽无法满足需求,此时需要给插槽命名,即具名插槽。子组件中用 <slot name="插槽名"> 预留出口,父组件中用 <template #插槽名>(或 <template v-slot:插槽名>)指定内容分发的目标插槽。
适用场景:子组件有多个自定义区域,比如卡片的头部、主体、底部,弹窗的标题、内容、按钮区等。
2. 实战案例(Vue3 + TS)
需求:封装一个高级卡片组件,父组件可分别自定义卡片的头部、主体、底部内容,实现多区域灵活扩展。
(1)子组件:CardNamed.vue(预留3个具名插槽)
vue
<script setup lang="ts">
// 子组件:定义3个具名插槽,分别对应头部、主体、底部
defineProps<{
// 组件自身props,与插槽独立
cardStyle?: {
width?: string
borderColor?: string
}
}>()
</script>
<template>
<div
class="card"
:style="{
border: `1px solid ${cardStyle?.borderColor || '#eee'}`,
padding: '20px',
borderRadius: '8px',
maxWidth: cardStyle?.width || '500px',
margin: '0 auto'
}"
>
<!-- 具名插槽:头部 -->
<slot name="header">
<h3 style="color: #333; margin: 0 0 15px 0;">默认头部标题</h3>
</slot>
<!-- 具名插槽:主体 -->
<slot name="content">
<p style="color: #666;">默认主体内容(父组件未提供时显示)</p>
</slot>
<!-- 具名插槽:底部 -->
<slot name="footer">
<div style="margin-top: 15px; text-align: right;">
<button style="padding: 6px 12px; background: #eee; border: none; border-radius: 4px;">
默认按钮
</button>
</div>
</slot>
</div>
</template>
(2)父组件:使用具名插槽
vue
<script setup lang="ts">
import CardNamed from '@/components/CardNamed.vue'
</script>
<template>
<div style="padding: 20px;">
<h2 style="text-align: center; margin-bottom: 30px;">具名插槽实战</h2>
<CardNamed :cardStyle="{ width: '600px', borderColor: '#42b983' }">
<!-- 头部插槽:使用 #header 简写(等价于 v-slot:header) -->
<template #header>
<div style="display: flex; justify-content: space-between; align-items: center;">
<h3 style="color: #42b983; margin: 0;">自定义卡片头部</h3>
<span style="color: #999; font-size: 14px;">2026-03-31</span>
</div>
</template>
<!-- 主体插槽:使用 v-slot:content 完整写法 -->
<template v-slot:content>
<div style="line-height: 1.8; color: #333;">
<p>1. 具名插槽可以实现多区域自定义,解决默认插槽只能有一个的问题</p>
<p>2. 父组件通过 template 标签 + #插槽名,指定内容分发的目标</p>
<p>3. 未自定义的插槽,会显示子组件中设置的默认内容</p>
</div>
</template>
<!-- 底部插槽:插入多个按钮 -->
<template #footer>
<div style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 15px;">
<button style="padding: 6px 12px; background: #eee; border: none; border-radius: 4px;">
取消
</button>
<button style="padding: 6px 12px; background: #42b983; color: white; border: none; border-radius: 4px;">
确认
</button>
</div>
</template>
</CardNamed>
</div>
</template>
3. 关键注意点
- 具名插槽的命名规范:建议使用语义化名称(如 header、content、footer),避免无意义命名(如 slot1、slot2)。
- 简写方式:<template #插槽名> 是 <template v-slot:插槽名> 的简写,Vue3 推荐使用简写,更简洁。
- 插槽顺序:父组件中插槽的顺序不影响渲染结果,渲染顺序由子组件中
<slot>标签的顺序决定。 - 默认插槽与具名插槽可共存:子组件中可同时有一个默认插槽和多个具名插槽,父组件中未用 template 包裹的内容,会自动分发到默认插槽。
四、高级用法:作用域插槽(带数据的插槽)
1. 核心特点
默认插槽和具名插槽,都是父组件向子组件传递"内容",但父组件无法直接访问子组件的数据;而作用域插槽,允许 子组件向父组件传递数据,父组件可以根据子组件传递的数据,自定义插槽内容的渲染方式。
简单说:作用域插槽 = 具名插槽 + 子组件数据传递,核心是"子传父数据,父自定义渲染"。
适用场景:子组件有复用的数据,但父组件需要不同的渲染样式,比如列表组件(子组件提供列表数据,父组件自定义列表项的渲染方式)、表格组件等。
2. 实战案例(Vue3 + TS)
需求:封装一个列表组件,子组件提供列表数据(用户列表),父组件可自定义列表项的渲染样式(比如有的场景显示头像+姓名,有的场景显示姓名+年龄)。
(1)子组件:ListScope.vue(传递数据给父组件)
vue
<script setup lang="ts">
// 子组件:定义列表数据,通过作用域插槽传递给父组件
interface User {
id: string
name: string
age: number
avatar: string
}
// 模拟列表数据(子组件内部数据,父组件无法直接访问)
const users: User[] = [
{ id: '1', name: '张三', age: 24, avatar: 'https://via.placeholder.com/40' },
{ id: '2', name: '李四', age: 22, avatar: 'https://via.placeholder.com/40' },
{ id: '3', name: '王五', age: 26, avatar: 'https://via.placeholder.com/40' }
]
// 作用域插槽:通过 slot 的 :user 传递数据(可传递多个数据,用逗号分隔)
// 这里的 user 就是子组件传递给父组件的数据
</script>
<template>
<div class="list" style="max-width: 600px; margin: 0 auto;">
<div class="list-header" style="font-weight: bold; padding: 10px; border-bottom: 1px solid #eee;">
用户列表(作用域插槽示例)
</div>
<div class="list-item" style="padding: 15px; border-bottom: 1px solid #f5f5f5;" v-for="user in users" :key="user.id">
<!-- 作用域插槽:传递子组件的 user 数据给父组件 -->
<slot name="item" :user="user" :isAdmin="user.age > 24">
<!-- 默认渲染:父组件未自定义时,显示默认样式 -->
<div style="display: flex; align-items: center; gap: 10px;">
<img :src="user.avatar" alt="头像" style="width: 40px; height: 40px; border-radius: 50%;">
<span>{{ user.name }}({{ user.age }}岁)</span>
</div>
</slot>
</div>
</div>
</template>
(2)父组件:使用作用域插槽(接收子组件数据)
vue
<script setup lang="ts">
import ListScope from '@/components/ListScope.vue'
// 父组件可定义自己的方法,结合子组件传递的数据使用
const formatAge = (age: number) => {
return age > 24 ? '成年' : '青年'
}
</script>
<template>
<div style="padding: 20px;">
<h2 style="text-align: center; margin-bottom: 30px;">作用域插槽实战</h2>
<!-- 场景1:自定义列表项样式(显示头像+姓名+年龄状态) -->
<ListScope>
<template #item="slotProps">
<!-- slotProps:接收子组件传递的所有数据({ user, isAdmin }) -->
<div style="display: flex; align-items: center; gap: 15px; padding: 5px 0;">
<img :src="slotProps.user.avatar" alt="头像" style="width: 40px; height: 40px; border-radius: 50%;">
<div>
<p style="margin: 0; font-weight: bold; color: #333;">{{ slotProps.user.name }}</p>
<p style="margin: 0; font-size: 14px; color: #666;">
年龄:{{ slotProps.user.age }}岁({{ formatAge(slotProps.user.age) }})
</p>
</div>
<span v-if="slotProps.isAdmin" style="margin-left: auto; padding: 2px 8px; background: #42b983; color: white; font-size: 12px; border-radius: 10px;">
管理员
</span>
</div>
</template>
</ListScope>
<div style="height: 40px;"></div>
<!-- 场景2:另一种自定义样式(仅显示姓名+年龄,无头像) -->
<ListScope>
<template #item="{ user, isAdmin }">
<!-- 解构 slotProps,直接使用 user 和 isAdmin,更简洁 -->
<div style="display: flex; justify-content: space-between; align-items: center; padding: 5px 0;">
<span style="color: #333;">{{ user.name }} - {{ user.age }}岁</span>
<span v-if="isAdmin" style="color: #42b983; font-size: 14px;">★ 管理员</span>
</div>
</template>
</ListScope>
</div>
</template>
3. 关键注意点
- 数据传递:子组件通过 的形式传递数据(绑定属性),父组件通过 template 的参数接收(如 #item="slotProps")。
- 解构简化:父组件可通过解构赋值,直接获取子组件传递的具体数据(如 #item="{ user, isAdmin }"),避免 slotProps.user 重复书写。
- 作用域边界:父组件中插槽内容的作用域仍属于父组件(可访问父组件的方法、数据),同时可访问子组件传递的 slotProps 数据,实现"父子数据互通"。
- 默认渲染:作用域插槽也可设置默认内容,当父组件未自定义插槽时,显示子组件中
<slot>内部的默认渲染样式。
五、三种插槽对比与高频避坑指南
1. 三种插槽核心对比
| 插槽类型 | 核心特点 | 适用场景 | 关键语法 |
|---|---|---|---|
| 默认插槽 | 无名称,仅一个出口,父传内容 | 子组件单个自定义区域 | 子:<slot>,父:直接写内容 |
| 具名插槽 | 有名称,多个出口,父传内容 | 子组件多个自定义区域 | 子:<slot name="xxx">,父:<template #xxx> |
| 作用域插槽 | 带数据传递,父子互通,父自定义渲染 | 子组件有复用数据,父需自定义渲染 | 子:<slot :key="value">,父:<template #xxx="slotProps"> |
2. 高频避坑点(必看)
(1)避坑1:作用域插槽未接收数据,导致报错
问题:子组件传递了数据,但父组件未通过 template 参数接收,直接使用子组件数据,导致"数据未定义"报错。
vue
<!-- 错误示例 -->
<ListScope>
<template #item>
<span>{{ user.name }}</span> <!-- 报错:user 未定义 -->
</template>
</ListScope>
<!-- 正确示例 -->
<ListScope>
<template #item="{ user }">
<span>{{ user.name }}</span> <!-- 正确,通过解构接收 user -->
</template>
</ListScope>
(2)避坑2:具名插槽未用 template 包裹,导致内容分发失败
问题:父组件使用具名插槽时,未将内容放在 <template #插槽名> 中,导致内容无法正确分发到对应插槽,默认渲染到默认插槽。
vue
<!-- 错误示例 -->
<CardNamed>
<div #header>自定义头部</div> <!-- 错误:未用 template 包裹 -->
</CardNamed>
<!-- 正确示例 -->
<CardNamed>
<template #header>
<div>自定义头部</div> <!-- 正确,用 template 包裹 -->
</template>
</CardNamed>
(3)避坑3:混淆作用域,父组件无法访问子组件未传递的数据
问题:父组件试图访问子组件中未通过插槽传递的数据,导致报错(作用域插槽只能访问子组件主动传递的 slotProps 数据)。
vue
<!-- 错误示例(子组件未传递 users 数组,仅传递了单个 user) -->
<ListScope>
<template #item="{ user }">
<span>{{ users.length }}</span> <!-- 报错:users 未定义 -->
</template>
</ListScope>
(4)避坑4:Vue3 中 v-slot 只能用在 template 标签上
问题:在 Vue3 中,v-slot(或 # 简写)只能用于 标签,不能直接用在普通标签上(Vue2 中可使用,Vue3 已废弃)。
六、总结:插槽实战核心要点
插槽的核心价值是「组件复用与灵活扩展」,三种插槽的使用逻辑的是"从简单到复杂",根据业务场景选择即可,无需过度复杂:
- 简单场景(单个自定义区域):用默认插槽,简洁高效。
- 多区域场景(多个自定义区域):用具名插槽,区分不同区域。
- 数据交互场景(子传父数据+父自定义渲染):用作用域插槽,实现父子数据互通与灵活渲染。
所有案例代码均基于 Vue3 + TypeScript 编写,经过实测可直接复制到项目中运行,无侵权、无错误。建议大家结合自己的业务场景,多练习插槽的使用,尤其是作用域插槽(中大型项目高频使用),逐步掌握组件封装的技巧。
插槽的使用没有固定模板,核心是"按需设计",只要能实现组件的灵活复用、降低代码冗余,就是合理的用法