第 118 章:插槽与作用域
学习目标
- 掌握插槽的各种用法(默认/具名/作用域)
- 理解插槽的渲染机制
- 学会 useSlots 与作用域插槽
- 掌握动态插槽与插槽透传
一、插槽基础
1.1 为什么需要插槽
插槽让父组件能自定义子组件的内容。
vue
<!-- Card.vue -->
<template>
<div class="card">
<slot></slot> <!-- 占位 -->
</div>
</template>vue
<!-- 使用 -->
<Card>
<h2>标题</h2>
<p>内容</p>
</Card>1.2 默认内容
vue
<!-- SubmitButton.vue -->
<template>
<button class="submit-btn">
<slot>提交</slot> <!-- 默认内容 -->
</button>
</template>vue
<!-- 不传内容显示默认 -->
<SubmitButton />
<!-- 传内容覆盖默认 -->
<SubmitButton>立即购买</SubmitButton>二、具名插槽
2.1 子组件定义多个插槽
vue
<!-- PageLayout.vue -->
<template>
<div class="page">
<header>
<slot name="header">默认头部</slot>
</header>
<main>
<slot /> <!-- 默认插槽 -->
</main>
<aside>
<slot name="sidebar" />
</aside>
<footer>
<slot name="footer">默认底部</slot>
</footer>
</div>
</template>2.2 父组件填充
vue
<PageLayout>
<!-- 默认插槽 -->
<template #default>
<p>主体内容</p>
</template>
<!-- 具名插槽 -->
<template #header>
<h1>网站标题</h1>
</template>
<template #sidebar>
<nav>侧边导航</nav>
</template>
<template #footer>
<p>© 2026</p>
</template>
</PageLayout>2.3 简写形式
vue
<!-- 完整写法 -->
<template v-slot:header>...</template>
<!-- 简写 -->
<template #header>...</template>
<!-- 默认插槽 -->
<template v-slot:default>...</template>
<template #default>...</template>三、作用域插槽
3.1 子组件传数据给插槽
vue
<!-- UserList.vue -->
<script setup lang="ts">
interface User {
id: number;
name: string;
age: number;
}
defineProps<{ users: User[] }>();
</script>
<template>
<ul>
<li v-for="user in users" :key="user.id">
<!-- 把 user 暴露给父组件 -->
<slot :user="user" :index="user.id" :isAdult="user.age >= 18">
{{ user.name }} <!-- 默认内容 -->
</slot>
</li>
</ul>
</template>3.2 父组件接收数据
vue
<UserList :users="userList">
<template #default="{ user, index, isAdult }">
<div>
<span>{{ index }}. {{ user.name }}</span>
<span v-if="isAdult">(成年)</span>
</div>
</template>
</UserList>3.3 解构重命名
vue
<template #default="{ user: u, index: i }">
<p>{{ i }}. {{ u.name }}</p>
</template>四、useSlots 与 useAttrs
4.1 useSlots
vue
<!-- FancyButton.vue -->
<script setup lang="ts">
import { useSlots, computed } from 'vue';
const slots = useSlots();
// 检测是否有插槽内容
const hasContent = computed(() => !!slots.default);
</script>
<template>
<button>
<span v-if="hasContent">
<slot />
</span>
<span v-else>默认按钮</span>
</button>
</template>4.2 useAttrs
vue
<!-- Wrapper.vue -->
<script setup lang="ts">
import { useAttrs } from 'vue';
const attrs = useAttrs();
console.log(attrs); // 所有非 props 的 attribute
</script>
<template>
<input v-bind="attrs" />
</template>五、动态插槽
5.1 动态插槽名
vue
<!-- TabContainer.vue -->
<script setup lang="ts">
import { ref } from 'vue';
const activeTab = ref('home');
</script>
<template>
<div>
<button @click="activeTab = 'home'">Home</button>
<button @click="activeTab = 'profile'">Profile</button>
<!-- 动态插槽 -->
<slot :name="activeTab" />
</div>
</template>vue
<TabContainer>
<template #[activeTab]>
<p>当前标签的内容</p>
</template>
</TabContainer>六、插槽透传
6.1 多层组件透传
vue
<!-- Inner.vue -->
<template>
<div class="inner">
<slot />
</div>
</template>vue
<!-- Middle.vue -->
<template>
<Inner>
<slot /> <!-- 透传给 Inner -->
</Inner>
</template>vue
<!-- App.vue -->
<template>
<Middle>
<p>最终内容</p>
</Middle>
</template>6.2 包裹并扩展
vue
<!-- EnhancedButton.vue -->
<script setup lang="ts">
defineProps<{ label: string }>();
</script>
<template>
<button>
<!-- 在前面加图标 -->
<span class="icon">🔥</span>
<!-- 透传默认插槽 -->
<slot>{{ label }}</slot>
</button>
</template>七、实战案例
7.1 数据表格
vue
<!-- DataTable.vue -->
<script setup lang="ts" generic="T extends Record<string, any>">
interface Column<T> {
key: keyof T;
title: string;
width?: string;
}
defineProps<{
data: T[];
columns: Column<T>[];
}>();
</script>
<template>
<table>
<thead>
<tr>
<th
v-for="col in columns"
:key="String(col.key)"
:style="{ width: col.width }"
>
{{ col.title }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, index) in data" :key="index">
<td v-for="col in columns" :key="String(col.key)">
<!-- 把行数据和列定义暴露出去 -->
<slot
:name="`cell-${String(col.key)}`"
:row="row"
:column="col"
:value="row[col.key]"
>
{{ row[col.key] }}
</slot>
</td>
</tr>
</tbody>
</table>
</template>7.2 使用
vue
<script setup lang="ts">
import DataTable from './DataTable.vue';
interface User {
id: number;
name: string;
age: number;
}
const users = ref<User[]>([
{ id: 1, name: 'Tom', age: 18 },
{ id: 2, name: 'Jerry', age: 20 }
]);
const columns = [
{ key: 'id', title: 'ID' },
{ key: 'name', title: '姓名' },
{ key: 'age', title: '年龄' }
];
</script>
<template>
<DataTable :data="users" :columns="columns">
<!-- 自定义姓名列 -->
<template #cell-name="{ row }">
<strong style="color: blue">{{ row.name }}</strong>
</template>
<!-- 自定义年龄列 -->
<template #cell-age="{ value }">
<span v-if="value >= 18" style="color: green">{{ value }} 岁</span>
<span v-else style="color: orange">{{ value }} 岁</span>
</template>
</DataTable>
</template>7.3 模态框
vue
<!-- Modal.vue -->
<script setup lang="ts">
defineProps<{ visible: boolean; title: string }>();
defineEmits<{ 'update:visible': [value: boolean] }>();
</script>
<template>
<Teleport to="body">
<div v-if="visible" class="modal-mask" @click.self="$emit('update:visible', false)">
<div class="modal-container">
<header class="modal-header">
<h2>{{ title }}</h2>
<button @click="$emit('update:visible', false)">×</button>
</header>
<main class="modal-body">
<slot /> <!-- 默认内容 -->
</main>
<footer class="modal-footer">
<slot name="footer">
<button @click="$emit('update:visible', false)">关闭</button>
</slot>
</footer>
</div>
</div>
</Teleport>
</template>vue
<Modal v-model:visible="showModal" title="确认操作">
<p>你确定要删除这条记录吗?</p>
<template #footer>
<button @click="showModal = false">取消</button>
<button @click="confirmDelete">确认</button>
</template>
</Modal>八、常见错误
8.1 模板插槽名拼写错误
vue
<!-- ❌ 子组件没有 header 插槽 -->
<template #headr>...</template>
<!-- ✅ 子组件定义了 header 插槽 -->
<template #header>...</template>8.2 作用域插槽没解构
vue
<!-- ❌ 直接用 slotProps.user -->
<UserList #default="slotProps">
{{ slotProps.user.name }}
</UserList>
<!-- ✅ 解构更清晰 -->
<UserList #default="{ user }">
{{ user.name }}
</UserList>8.3 多个默认插槽
vue
<!-- ❌ 不允许 -->
<template>
<MyComponent>
<template #default>A</template>
<template #default>B</template>
</MyComponent>
</template>九、本章小结
| 类型 | 用途 | 语法 |
|---|---|---|
| 默认插槽 | 一处可定制内容 | <slot /> |
| 具名插槽 | 多处不同位置内容 | <slot name="x" /> |
| 作用域插槽 | 子传数据给父 | <slot :data="x" /> |
| 动态插槽 | 运行时决定插槽 | <template #[name]> |
| 插槽透传 | 多层组件传内容 | 直接 <slot /> |
动手练习
- 数据列表:实现一个列表组件,父组件自定义每一项的渲染
- 表格:写一个通用表格组件,支持自定义列渲染
- 模态框:实现一个 Modal,支持自定义 header/body/footer
- 布局组件:实现一个 Layout,支持多个插槽透传
推荐阅读
- 📖 Vue 3 插槽 — 官方文档
- 📖 Vue 3 作用域插槽 — 详解
- 🌐 Vue 3 Slots Examples — 官方示例
下一章:第 119 章:组件通信模式 →