Vue.js: 插槽 slots
最后更新:2026-08-26
插槽(Slot)是 Vue 的内容分发 API——父组件可以往子组件的"槽位"插入任意内容。插槽让组件像"模板"一样灵活:父组件控制显示什么,子组件控制显示在哪里。
Vue 3 提供了 3 种插槽:默认插槽、具名插槽、作用域插槽。本课帮你掌握这 3 种用法和最佳实践。
1. 你将学到
- 默认插槽
<slot />的基本用法 - 具名插槽
<slot name="header" />和v-slot:header简写 - 作用域插槽:子组件传数据给父组件插槽
<template v-slot>语法(Vue 2.6+)- 动态插槽名和缩写语法
- 默认插槽内容(fallback content)
$slots/useSlots钩子
2. 一个 Card 组件无法定制内容的难题
(1) 痛点:5 种 Card 样式,要写 5 个组件
Alice 的电商后台需要 5 种卡片:
VUE
<!-- ❌ 翻车版:5 个组件 -->
<!-- ProductCard.vue -->
<div class="card">{{ product.name }}</div>
<!-- UserCard.vue -->
<div class="card">{{ user.name }}</div>
<!-- OrderCard.vue -->
<div class="card">Order #{{ order.id }}</div>
<!-- 5 个组件,90% 代码相同,只有内容不同 -->
产品经理 Charlie:
"Alice,下周还要加 5 种卡片类型。不能一直加新组件。我们需要一个灵活的 Card,能接收任何内容。"
(2) Vue 插槽解法:1 个 BaseCard,父组件塞内容
VUE
<!-- BaseCard.vue - 通用卡片,插槽接收任何内容 -->
<template>
<div class="card">
<div v-if="$slots.header" class="card-header">
<slot name="header" />
</div>
<div class="card-body">
<slot /> <!-- 默认插槽 -->
</div>
<div v-if="$slots.footer" class="card-footer">
<slot name="footer" />
</div>
</div>
</template>
VUE
<!-- ProductCard 使用 BaseCard -->
<BaseCard>
<template #header>
<h3>Product</h3>
</template>
<p>iPhone 15 Pro</p> <!-- 默认插槽 -->
<template #footer>
<button>Add to Cart</button>
</template>
</BaseCard>
1 个 BaseCard.vue → 无限种卡片。每次新增只需写内容,不用改组件。
(3) 收益
使用插槽后:
- 组件数:5 → 1(-80%)
- 新增卡片类型:1 行模板代码
- 维护成本:改 1 处样式,5+ 卡片同步更新
- 可复用性:BaseCard 可用于所有卡片场景
3. 默认插槽
(1) 基本用法
VUE
<!-- BaseCard.vue 子组件 -->
<template>
<div class="card">
<!-- 插槽:父组件可以插入任何内容 -->
<slot />
</div>
</template>
VUE
<!-- App.vue 父组件 -->
<template>
<BaseCard>
<h3>Hello</h3>
<p>This is the card content</p>
</BaseCard>
</template>
渲染结果:
HTML
<div class="card">
<h3>Hello</h3>
<p>This is the card content</p>
</div>
(2) 默认内容(fallback)
VUE
<!-- 子组件 -->
<template>
<div class="card">
<slot>
<!-- 默认内容:父组件不传时显示 -->
<p>No content provided</p>
</slot>
</div>
</template>
VUE
<!-- 父组件不传内容 -->
<BaseCard />
<!-- 渲染:<p>No content provided</p> -->
<!-- 父组件传内容 -->
<BaseCard>
<p>Custom content</p>
</BaseCard>
<!-- 渲染:<p>Custom content</p>(覆盖默认)-->
(3) 5 大使用场景
| 场景 | 用法 |
|---|---|
| 卡片内容 | <slot /> |
| 按钮文字 | <slot /> |
| 列表项 | <slot :item="item" /> |
| 表单字段 | <slot /> |
| 模态框主体 | <slot /> |
4. 具名插槽
(1) 定义具名插槽
VUE
<!-- BaseLayout.vue 子组件 -->
<template>
<div class="layout">
<header>
<slot name="header" />
</header>
<main>
<slot /> <!-- 默认插槽(可省略 name="default") -->
</main>
<footer>
<slot name="footer" />
</footer>
</div>
</template>
(2) 父组件使用(3 种语法)
VUE
<!-- App.vue 父组件 -->
<!-- 写法 1:v-slot:name(最完整) -->
<BaseLayout>
<template v-slot:header>
<h1>My App</h1>
</template>
<template v-slot:default>
<p>Main content</p>
</template>
<template v-slot:footer>
<p>Footer</p>
</template>
</BaseLayout>
<!-- 写法 2:缩写 #name(推荐) -->
<BaseLayout>
<template #header>
<h1>My App</h1>
</template>
<p>Main content</p> <!-- 默认插槽可省略 template -->
<template #footer>
<p>Footer</p>
</template>
</BaseLayout>
<!-- 写法 3:v-slot 接收多个插槽对象(Vue 3 推荐) -->
<BaseLayout>
<template #header>
<h1>My App</h1>
</template>
<template #default="{ user }"> <!-- 解构作用域插槽 -->
<p>Hello, {{ user.name }}</p>
</template>
<template #footer>
<button>Logout</button>
</template>
</BaseLayout>
(3) 动态插槽名
VUE
<!-- 子组件 -->
<template>
<div>
<slot :name="slotName" />
</div>
</template>
<script setup>
const { ref } = Vue
const dynamicSlotName = ref('header')
</script>
<!-- 父组件:动态绑定插槽 -->
<BaseLayout>
<template #[dynamicSlotName]>
<p>Dynamic content</p>
</template>
</BaseLayout>
(4) 检测插槽存在
VUE
<!-- 子组件:检查插槽是否传入 -->
<template>
<div class="card">
<div v-if="$slots.header" class="card-header">
<slot name="header" />
</div>
<slot />
</div>
</template>
VUE
<!-- 父组件 -->
<BaseCard>
<template #header>...</template> <!-- 传了 header 插槽,显示 -->
</BaseCard>
<BaseCard>
<!-- 没传 header 插槽,不显示 card-header -->
</BaseCard>
5. 作用域插槽
(1) 核心:子组件传数据给父组件插槽
VUE
<!-- 子组件:UserList.vue -->
<template>
<ul>
<li v-for="user in users" :key="user.id">
<!-- 把 user 传给父组件插槽 -->
<slot :user="user" :index="index" />
</li>
</ul>
</template>
<script setup>
defineProps({ users: Array })
</script>
VUE
<!-- 父组件:App.vue -->
<template>
<UserList :users="users">
<!-- 接收子组件传的数据 -->
<template #default="{ user, index }">
<p>{{ index + 1 }}. {{ user.name }} ({{ user.email }})</p>
</template>
</UserList>
</template>
渲染结果:
HTML
<ul>
<li><p>1. Alice (alice@example.com)</p></li>
<li><p>2. Bob (bob@example.com)</p></li>
<li><p>3. Charlie (charlie@example.com)</p></li>
</ul>
(2) 5 种作用域插槽模式
| 模式 | 父组件写法 | 用途 |
|---|---|---|
| 接收所有 props | <template #default="slotProps"> |
接收整个对象 |
| 解构 | <template #default="{ user }"> |
只用需要的字段 |
| 重命名 | <template #default="{ user: u }"> |
避免变量冲突 |
| 默认值 | <template #default="{ user = defaultUser }"> |
解构时给默认值 |
| 不用 template | {{ slotProps.user.name }} |
简单场景(默认插槽) |
(3) 完整示例:可定制列表
VUE
<!-- UserList.vue 子组件 -->
<template>
<ul class="user-list">
<li v-for="(user, index) in users" :key="user.id">
<slot :user="user" :index="index" :isAdmin="user.role === 'admin'" />
</li>
</ul>
</template>
<script setup>
defineProps({ users: Array })
</script>
VUE
<!-- 父组件使用(3 种方式) -->
<template>
<!-- 方式 1:简单展示 -->
<UserList :users="users">
<template #default="{ user }">
{{ user.name }}
</template>
</UserList>
<!-- 方式 2:表格展示 -->
<UserList :users="users">
<template #default="{ user, index, isAdmin }">
<tr>
<td>{{ index + 1 }}</td>
<td>{{ user.name }}</td>
<td>{{ user.email }}</td>
<td>
<span v-if="isAdmin" class="badge admin">Admin</span>
<span v-else class="badge user">User</span>
</td>
</tr>
</template>
</UserList>
<!-- 方式 3:卡片展示 -->
<UserList :users="users">
<template #default="{ user }">
<UserCard :user="user" />
</template>
</UserList>
</template>
6. $slots 和 useSlots
(1) $slots 访问插槽
VUE
<!-- 子组件 -->
<template>
<div>
<!-- 列出所有传入的插槽名 -->
<div v-for="(_, name) in $slots" :key="name">
<slot :name="name" />
</div>
</div>
</template>
(2) useSlots(Composition API)
VUE
<script setup>
const { useSlots } = Vue
const slots = useSlots()
// 检查插槽是否存在
if (slots.header) {
console.log('Header slot exists')
}
// 动态渲染
if (slots.default) {
console.log('Default slot exists')
}
</script>
(3) $slots vs useSlots
| 维度 | $slots |
useSlots() |
|---|---|---|
| API 类型 | 模板中直接用 | JS 中用(Composition API) |
| 返回 | 插槽对象 | 插槽对象(refs 形式) |
| 场景 | template 内 | <script setup> 内 |
7. 完整示例:可定制 Card 组件
▶ 示例:BaseCard 默认 + 具名插槽
HTML
📖 仅展示
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<style>
.card { border: 1px solid #e5e7eb; border-radius: 8px; overflow: hidden; margin: 0.5rem 0; }
.card-header, .card-footer { background: #f9fafb; padding: 0.75rem; font-weight: bold; }
.card-body { padding: 1rem; }
.btn { padding: 6px 12px; background: #42b883; color: white; border: none; border-radius: 4px; cursor: pointer; }
</style>
<div id="app">
<!-- 1. 最简单(只传 body) -->
<base-card>
<p>Simple content (only body)</p>
</base-card>
<!-- 2. 带 header -->
<base-card>
<template #header>
<h3>Product Name</h3>
</template>
<p>Product description</p>
</base-card>
<!-- 3. 完整 header + body + footer -->
<base-card>
<template #header>
<h3>Order #12345</h3>
</template>
<p>Total: $99.99</p>
<template #footer>
<button class="btn">View Details</button>
</template>
</base-card>
</div>
<script>
const { createApp } = Vue
// 子组件:BaseCard
const BaseCard = {
template: `
<div class="card">
<div v-if="$slots.header" class="card-header">
<slot name="header" />
</div>
<div class="card-body">
<slot>
<p>No content (默认内容)</p>
</slot>
</div>
<div v-if="$slots.footer" class="card-footer">
<slot name="footer" />
</div>
</div>
`
}
const App = { components: { BaseCard } }
createApp(App).mount('#app')
</script>
▶ 示例:作用域插槽(DataTable 自定义列)
HTML
📖 仅展示
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<style>
table { width: 100%; border-collapse: collapse; }
th, td { padding: 8px; border: 1px solid #ddd; text-align: left; }
th { background: #f9fafb; }
.badge { padding: 2px 8px; border-radius: 4px; color: white; font-size: 0.85rem; }
.badge.active { background: #10b981; }
.badge.inactive { background: #6b7280; }
.btn { padding: 4px 8px; background: #3b82f6; color: white; border: none; border-radius: 4px; cursor: pointer; }
</style>
<div id="app">
<data-table :columns="columns" :data="users"></data-table>
</div>
<script>
const { createApp } = Vue
// 子组件:DataTable(作用域插槽 + 默认插槽 fallback)
const DataTable = {
props: {
columns: { type: Array, required: true },
data: { type: Array, required: true }
},
template: `
<table>
<thead>
<tr>
<th v-for="col in columns" :key="col.key">{{ col.label }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, index) in data" :key="row.id">
<td v-for="col in columns" :key="col.key">
<!-- 作用域插槽:传 row/index/value 给父组件 -->
<slot :name="'cell-' + col.key" :row="row" :index="index" :value="row[col.key]">
<!-- 默认显示:父组件未定义该列插槽时 -->
{{ row[col.key] }}
</slot>
</td>
</tr>
</tbody>
</table>
`
}
// 父组件:自定义 status 和 actions 列
const App = {
components: { DataTable },
setup() {
const columns = [
{ key: 'id', label: 'ID' },
{ key: 'name', label: 'Name' },
{ key: 'email', label: 'Email' },
{ key: 'status', label: 'Status' },
{ key: 'actions', label: 'Actions' }
]
const users = [
{ id: 1, name: 'Alice', email: 'alice@x.com', status: 'active' },
{ id: 2, name: 'Bob', email: 'bob@x.com', status: 'inactive' }
]
return { columns, users }
},
methods: {
editUser(row) { console.log('Edit:', row.name) }
},
template: `
<data-table :columns="columns" :data="users">
<!-- 自定义 status 列 -->
<template #cell-status="{ row }">
<span :class="['badge', row.status]">{{ row.status }}</span>
</template>
<!-- 自定义 actions 列 -->
<template #cell-actions="{ row }">
<button class="btn" @click="editUser(row)">Edit</button>
</template>
</data-table>
`
}
createApp(App).mount('#app')
</script>
▶ 示例:5 种 BaseCard 使用方式速查(纯展示)
| 方式 | 父组件模板 | 用途 |
|---|---|---|
| 1. 最简单(只传 body) | <BaseCard><p>Simple</p></BaseCard> |
只用默认插槽 |
| 2. 带 header | <BaseCard><template #header>...</template>...</BaseCard> |
加头部 |
| 3. 完整 header + body + footer | 三段 <template #name> |
完整布局 |
| 4. 完整写法(v-slot) | <template v-slot:header> |
长写法(旧文档) |
| 5. 作用域插槽 | <template #actions="{ user }"> |
子→父传数据 |
▶ 示例:5 个常见错误速查
| 错误 | 现象 | 解决 |
|---|---|---|
| 插槽名拼错 | 内容不显示 | 检查 <slot name="..."> 和 #name 一致 |
| 父组件多个根元素 | 警告 | 父组件用 <template #name> 包装 |
| v-slot 旧语法 | Vue 2 兼容 | 改用 v-slot:header 或 #header |
| 默认插槽没传 | 父组件不显示 | 检查父组件的 <Component /> 内容 |
| 作用域插槽不解构 | 模板冗长 | 用 { user, index } 解构 |
▶ 示例:5 个性能对比
| 写法 | 编译 | 性能 | 推荐 |
|---|---|---|---|
<slot /> |
静态 | ⭐⭐⭐⭐⭐ | 默认 |
<slot name="x" /> |
静态 | ⭐⭐⭐⭐⭐ | 具名 |
<slot :x="x" /> |
静态 | ⭐⭐⭐⭐ | 作用域 |
v-if="$slots.x" |
条件 | ⭐⭐⭐⭐ | 检测存在 |
useSlots() |
运行时 | ⭐⭐⭐ | JS 中用 |
▶ 示例:$slots 5 大用途
HTML
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<div id="app">
<smart-wrapper>
<template #header>Header 内容</template>
<p>默认插槽:主要内容</p>
<template #footer>Footer 内容</template>
</smart-wrapper>
</div>
<script>
const { createApp } = Vue
const SmartWrapper = {
template: `
<div style="border: 1px solid #ddd; padding: 1rem; border-radius: 8px;">
<div v-if="slots.header" style="background: #f9fafb; padding: 8px;">
<slot name="header" />
</div>
<div style="padding: 8px;">
<slot>
<p>Default fallback</p>
</slot>
</div>
<div v-if="slots.footer" style="background: #f9fafb; padding: 8px;">
<slot name="footer" />
</div>
<p style="color: #999; font-size: 0.85rem;">
已检测插槽: {{ Object.keys(slots).join(', ') }}
</p>
</div>
`,
computed: {
slots() { return this.$slots }
}
}
const App = { components: { SmartWrapper } }
createApp(App).mount('#app')
</script>
❓ 常见问题
Q 插槽和 props 有什么区别?
A props 传数据(任何 JS 值),插槽传内容(HTML/组件片段)。Props 适合配置项,插槽适合内容定制。
Q 作用域插槽什么时候用?
A 当父组件需要基于子组件的数据自定义渲染时。例如:表格的列渲染、列表的项渲染、卡片的内容布局。
Q v-slot 和 # 缩写区别?
A 完全等价,
#header 是 v-slot:header 的简写。推荐用缩写(更简洁)。Q 默认插槽可以命名吗?
A 默认插槽不需要命名,
<slot /> 就是默认。如果非要命名,用 <slot name="default" />,父组件用 <template #default> 或直接传内容。Q 插槽能在 props 中传递吗?
A 不能直接 prop 插槽。但可以 prop 一个 VNode / Render 函数(高级用法)。普通场景用
<slot> 即可。Q 怎么在父组件中检测子组件的插槽?
A 用
$slots(template)或 useSlots()(JS)。子组件用 <slot> 暴露,父组件用 <template #name> 填充。Q 具名插槽可以有默认内容吗?
A 可以。
<slot name="header"><h3>Default Header</h3></slot>。当父组件不传 header 时显示。📖 小节
- 插槽是 Vue 的内容分发 API:父组件控制内容,子组件控制位置
- 3 种插槽:默认插槽(
<slot />)、具名插槽(name="x")、作用域插槽(:x="x") - 父组件用
<template #name>或缩写#name传递具名插槽 - 作用域插槽让子组件传数据给父组件(slot props)
<slot>默认内容(fallback)当父组件不传时显示$slots/useSlots()检查插槽存在- 插槽和 props 配合:props 传配置,插槽传内容
📝 作业
-
基础题(难度⭐) 实现一个简单的 Button 组件:
- BaseButton.vue:默认插槽 + 3 种 variant(primary/success/danger)
- props:
variant(String) - 父组件:测试 3 种 variant + 自定义插槽内容
-
进阶题(难度⭐⭐) 实现 BaseLayout 组件:
- 子组件:3 个具名插槽(header / default / footer)
- 父组件:使用 BaseLayout 实现 Dashboard 页面
- 用
$slots检测插槽是否存在 - 添加默认内容(fallback)
-
挑战题(难度⭐⭐⭐) 实现完整的 DataTable 组件系统:
- DataTable.vue:接收 columns + data,渲染表格
- 作用域插槽:
#cell-{key}允许父组件自定义单元格 - 默认插槽:父组件不提供时显示原始值
- 父组件示例:用户表 + 订单表,2 种不同的列渲染
- 性能:v-memo 缓存行(数据不变时跳过重渲染)
- TypeScript 强类型