
Vue插槽从入门到项目实战:默认插槽、具名插槽、作用域插槽,一次讲明白
你肯定遇到过这种情况:写了一个弹窗组件,长宽、遮罩、关闭按钮都挺好,但是弹窗里显示的内容每个地方不一样------有时候是表单,有时候是一段文字,有时候是一张图片。
如果不用插槽,你可能会疯狂地通过 props 传配置,甚至传 HTML 字符串,又丑又难维护。但有了插槽,一切就优雅了:组件提供"占位符",父组件往里面塞任何东西,包括组件、HTML、文本,甚至还能拿到子组件的数据。
下面我们就用案例,一个一个搞清楚 Vue 的几种插槽。
一、默认插槽:最简单的占位
场景
一个通用的卡片组件,卡片的外框、标题、边框都一样,但中间的内容可以是文字、按钮、甚至一张图片。
1. 子组件 Card.vue
vue
<template>
<div class="card">
<div class="card-header">
<h3>我是卡片标题</h3>
</div>
<div class="card-body">
<!--
这里就是插槽的出口,<slot></slot> 是一个占位符
父组件在使用 <Card> 时,放在它里面的内容会出现在这里
如果父组件没传内容,就显示“默认的卡片内容”
-->
<slot>默认的卡片内容</slot>
</div>
</div>
</template>
<script setup>
// 这个组件里没什么逻辑,只负责提供一个“壳”
</script>
<style scoped>
.card {
border: 1px solid #ccc;
border-radius: 8px;
overflow: hidden;
margin: 10px;
}
.card-header {
background: #f0f0f0;
padding: 10px;
}
.card-body {
padding: 15px;
}
</style>
2. 父组件 App.vue
vue
<template>
<div class="app">
<!-- 第一个卡片:里面塞了一段文字 -->
<Card>
<p>这是一段自定义的卡片内容,来自于父组件。</p>
</Card>
<!-- 第二个卡片:里面塞了一个按钮 -->
<Card>
<button @click="sayHello">点我打招呼</button>
</Card>
<!-- 第三个卡片:啥也不传,显示默认内容 -->
<Card></Card>
</div>
</template>
<script setup>
import Card from './components/Card.vue'
function sayHello() {
alert('你好!')
}
</script>
案例小结: <slot></slot> 就是默认插槽。父组件在子组件标签里写的所有内容,都会出现在这个位置。子组件通过 <slot>默认内容</slot> 可以提供后备内容。
二、具名插槽:多个占位,精确控制
场景
一个页面布局组件,有头部、侧边栏、主内容区、底部四个区域。父组件需要分别往这四个地方塞不同内容。
1. 子组件 Layout.vue
vue
<template>
<div class="layout">
<!-- 头部区域 -->
<header class="layout-header">
<!-- 给 slot 一个 name 属性,这就是具名插槽 -->
<slot name="header">默认头部</slot>
</header>
<div class="layout-middle">
<!-- 侧边栏区域 -->
<aside class="layout-sidebar">
<slot name="sidebar">默认侧边栏</slot>
</aside>
<!-- 主内容区域,没有名字的默认插槽 -->
<main class="layout-main">
<!-- 默认插槽,name 默认为 "default" -->
<slot>默认主内容</slot>
</main>
</div>
<!-- 底部区域 -->
<footer class="layout-footer">
<slot name="footer">默认底部</slot>
</footer>
</div>
</template>
<script setup>
</script>
<style scoped>
.layout-header, .layout-footer {
padding: 10px;
background: #e9ecef;
text-align: center;
}
.layout-middle {
display: flex;
}
.layout-sidebar {
width: 200px;
padding: 10px;
background: #f8f9fa;
}
.layout-main {
flex: 1;
padding: 10px;
}
</style>
2. 父组件 App.vue
vue
<template>
<Layout>
<!--
通过 v-slot:插槽名 或 #插槽名 来指定内容放到哪个插槽
#是v-slot的简写
-->
<template #header>
<h1>网站标题(父组件提供)</h1>
</template>
<!-- 侧边栏内容 -->
<template #sidebar>
<ul>
<li>导航一</li>
<li>导航二</li>
</ul>
</template>
<!-- 默认插槽内容,可以直接写,不用包 template 也行 -->
<div>
<p>这里是主内容区域,可以放任何东西。</p>
<p>包括其他组件、文本、图片等。</p>
</div>
<!-- 底部内容 -->
<template #footer>
<small>© 2025 我的公司</small>
</template>
</Layout>
</template>
<script setup>
import Layout from './components/Layout.vue'
</script>
案例小结: 具名插槽用 <slot name="xxx"> 定义,父组件用 <template v-slot:xxx> 或 <template #xxx> 往里面填内容。没有名字的就是默认插槽。
三、作用域插槽:子组件的数据,父组件来决定怎么展示
这是插槽里最强大、也最容易让新手懵的地方。一句话:数据在子组件里,但是渲染成什么样由父组件决定。
场景
一个商品列表组件,它负责获取数据和循环,但每一项的外观(是卡片、行、还是列表)由使用它的父组件决定。
1. 子组件 ProductList.vue
vue
<template>
<div>
<h2>商品列表</h2>
<!-- 循环商品数据 -->
<div v-for="(product, index) in products" :key="product.id" class="product-item">
<!--
关键在这里:<slot> 除了占位置,还可以绑定属性传给父组件
:product="product" 表示把当前循环的 product 对象暴露出去
:index="index" 还可以暴露下标
-->
<slot name="product" :product="product" :index="index">
<!-- 后备内容:简单显示名字 -->
{{ product.name }}
</slot>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
// 模拟商品数据
const products = ref([
{ id: 1, name: '键盘', price: 299 },
{ id: 2, name: '鼠标', price: 99 },
{ id: 3, name: '显示器', price: 1299 }
])
</script>
2. 父组件 App.vue
vue
<template>
<div>
<!-- 第一种:用卡片方式展示 -->
<ProductList>
<!--
v-slot:product="slotProps"
slotProps 就是子组件 <slot> 上绑定的所有属性的集合
这里 slotProps 包含 { product, index }
-->
<template #product="{ product, index }">
<div style="border:1px solid #ddd; padding:10px; margin:5px;">
<strong>#{{ index + 1 }}</strong>
<span>{{ product.name }}</span>
<span style="color:red;">¥{{ product.price }}</span>
</div>
</template>
</ProductList>
<hr />
<!-- 第二种:用纯文字列表 -->
<ProductList>
<template #product="{ product }">
<p>📦 {{ product.name }} —— {{ product.price }}元</p>
</template>
</ProductList>
</div>
</template>
<script setup>
import ProductList from './components/ProductList.vue'
</script>
案例小结: 子组件通过 <slot :数据名="数据值"> 把数据传给父组件,父组件通过 v-slot="slotProps" 接收,然后可以随意解构使用。这样父组件就拥有了对子组件数据的"渲染控制权"。
四、动态插槽:插槽名也可以变
有时候一个组件会渲染多个不同名的插槽,但父组件想用同一个模板去填充,只是名字不同,这时可以用动态插槽。
vue
<!-- 子组件 -->
<template>
<div>
<slot name="tab-home">首页</slot>
<slot name="tab-about">关于</slot>
</div>
</template>
父组件可以这样动态匹配:
vue
<template>
<MyTabs>
<!-- [动态插槽名] 会根据 tabName 的值变化 -->
<template v-for="tab in tabs" :key="tab.slotName" #[tab.slotName]>
<p>{{ tab.content }}</p>
</template>
</MyTabs>
</template>
<script setup>
import { ref } from 'vue'
const tabs = ref([
{ slotName: 'tab-home', content: '首页内容' },
{ slotName: 'tab-about', content: '关于内容' }
])
</script>
本质还是具名插槽,只是名字用了变量。
练习题
选择题
-
在子组件中,用来定义插槽的标签是( )
A.
<template>B.
<slot>C.
<v-slot>D.
<content> -
父组件使用具名插槽时,正确的写法是( )
A.
<slot name="header">B.
<template slot="header">C.
<template v-slot:header>或<template #header>D.
<div v-slot="header"> -
作用域插槽的核心作用是( )
A. 子组件可以修改父组件的数据
B. 父组件可以拿到子组件的数据,并决定如何渲染
C. 插槽可以动态切换名称
D. 子组件可以向父组件传递事件
判断题
-
一个组件里只能有一个
<slot>。( ) -
作用域插槽中,父组件拿到的数据是只读的,不能修改。( )
编程题
-
实现一个弹窗组件
Modal:-
弹窗有一个头部(标题)和主体内容,以及底部按钮区
-
头部标题父组件可以通过 prop 传递,但也可以使用具名插槽覆盖
-
主体内容完全由父组件通过默认插槽决定
-
底部按钮区由父组件通过具名插槽
footer填充,如果没传则默认显示"关闭"按钮 -
弹窗的显示/隐藏由父组件通过
v-model控制
-
-
实现一个表格组件
DataTable:-
表格接收一个
data数组(每项是对象)和一个columns配置数组(如[{key: 'name', title: '姓名'}, ...]) -
默认情况下,表格用
v-for渲染行和列,显示文本 -
额外要求:允许父组件通过作用域插槽自定义某个列的渲染方式(比如把"姓名"列渲染成链接)
-
答案
-
B
-
C
-
B
-
错误,可以有多个具名插槽和一个默认插槽。
-
正确,子组件传给插槽的数据应该由子组件修改,父组件不应直接改动,除非子组件提供了方法。
-
参考实现:
Modal.vue
vue
<template>
<div v-if="modelValue" class="modal-mask" @click.self="close">
<div class="modal-box">
<div class="modal-header">
<!-- 具名插槽 header,后备内容为 props.title -->
<slot name="header">{{ title }}</slot>
</div>
<div class="modal-body">
<!-- 默认插槽 -->
<slot>主体内容</slot>
</div>
<div class="modal-footer">
<slot name="footer">
<button @click="close">关闭</button>
</slot>
</div>
</div>
</div>
</template>
<script setup>
defineProps({
modelValue: Boolean,
title: { type: String, default: '弹窗标题' }
})
const emit = defineEmits(['update:modelValue'])
function close() {
emit('update:modelValue', false)
}
</script>
<style scoped>
.modal-mask {
position: fixed; top:0; left:0; width:100%; height:100%;
background: rgba(0,0,0,0.5); display:flex; align-items:center; justify-content:center;
}
.modal-box {
background: white; border-radius: 8px; min-width: 300px;
}
.modal-header, .modal-body, .modal-footer { padding: 15px; }
</style>
父组件使用
vue
<Modal v-model="showModal" title="用户登录">
<template #header>
<h2>自定义头部(覆盖了title prop)</h2>
</template>
<form>...表单内容...</form>
<template #footer>
<button @click="showModal=false">确定</button>
<button @click="showModal=false">取消</button>
</template>
</Modal>
-
参考实现:
DataTable.vue
vue
<template>
<table>
<thead>
<tr><th v-for="col in columns" :key="col.key">{{ col.title }}</th></tr>
</thead>
<tbody>
<tr v-for="(row, rowIndex) in data" :key="rowIndex">
<td v-for="col in columns" :key="col.key">
<!-- 这里定义了作用域插槽,名字为 'cell-'+col.key -->
<slot :name="'cell-' + col.key" :row="row" :value="row[col.key]" :col="col">
{{ row[col.key] }}
</slot>
</td>
</tr>
</tbody>
</table>
</template>
<script setup>
defineProps({
data: { type: Array, required: true },
columns: { type: Array, required: true }
})
</script>
父组件使用(自定义姓名列)
vue
<DataTable :data="users" :columns="columns">
<!-- 动态插槽名,针对 'cell-name' 进行自定义 -->
<template #cell-name="{ row, value }">
<a :href="'/user/' + row.id">{{ value }}</a>
</template>
</DataTable>
<script setup>
import { ref } from 'vue'
const users = ref([
{ id: 1, name: 'Alice', age: 25 },
{ id: 2, name: 'Bob', age: 30 }
])
const columns = [
{ key: 'name', title: '姓名' },
{ key: 'age', title: '年龄' }
]
</script>
写在最后
插槽是 Vue 组件复用中非常核心的一环,它让组件从"写死"变成了"可定制"。初学作用域插槽可能会有点绕,记住一句话:数据在孩子手里,但是家长决定孩子穿什么衣服。 多写几个弹窗、列表、布局组件,你就能体会到它的强大了。
老规矩,有问题评论区见,我挨个回。下篇咱们聊生命周期或者动画。