一、插槽
如果说 props 是让父组件给子组件传“数据”,那么插槽就是让父组件给子组件传“HTML 内容”。
为了让你秒懂,我们用一个生活中的例子来理解:
插槽就像是一台“游戏机”(子组件),游戏机本身只负责提供外壳和运行逻辑,但里面留了一个“卡槽”(插槽)。你可以往里面插不同的“游戏卡带”(父组件传入的 HTML 内容),从而让这台游戏机展现出不同的游戏画面。
在 Vue 3 中,插槽主要分为三大类。
1. 默认插槽(Default Slot)
场景:子组件只需要一个“坑位”来接收父组件的内容。
子组件 (MyCard.vue):用 <slot></slot> 占位。你可以把它想象成一个“默认内容”,如果父组件没传东西,就显示默认内容。
<template>
<div class="card">
<!-- 插槽出口:父组件传的内容会渲染在这里 -->
<slot>
<p>这是默认内容,如果父组件不传,就显示我</p>
</slot>
</div>
</template>
父组件:直接在子组件标签内部写 HTML 即可。
<template>
<MyCard>
<h2>我是父组件传进来的标题</h2>
<p>我是父组件传进来的正文</p>
</MyCard>
</template>
2. 具名插槽(Named Slots)
场景:子组件有多个“坑位”。比如一个后台布局组件,需要分别接收“头部”、“侧边栏”和“主内容区”。
子组件 (BaseLayout.vue):通过 name 属性给插槽命名。
<template>
<div class="layout">
<header>
<slot name="header"></slot> <!-- 头部坑位 -->
</header>
<main>
<slot></slot> <!-- 没写 name 的,就是默认坑位 -->
</main>
<footer>
<slot name="footer"></slot> <!-- 底部坑位 -->
</footer>
</div>
</template>
父组件:使用 <template> 标签配合 v-slot(简写为 #)来精准投放内容。
<template>
<BaseLayout>
<!-- 投放到 header 插槽 -->
<template #header>
<h1>后台管理系统</h1>
</template>
<!-- 投放到默认插槽 -->
<p>这里是页面的主要内容区域</p>
<!-- 投放到 footer 插槽 -->
<template #footer>
<span>Copyright © 2026</span>
</template>
</BaseLayout>
</template>
3. 作用域插槽(Scoped Slots)—— 进阶核心
场景:子组件不仅提供“坑位”,还把子组件内部的数据传回给父组件,让父组件根据这些数据来决定“坑位里长什么样”。
最经典的实际开发场景:封装一个通用的列表组件。
子组件 (UserList.vue):在 <slot> 上绑定属性,把内部数据暴露出去。
<template>
<ul>
<li v-for="user in users" :key="user.id">
<!-- 把当前遍历的 user 对象作为插槽 prop 传出去 -->
<slot name="userItem" :user="user">
{{ user.name }} <!-- 默认显示名字 -->
</slot>
</li>
</ul>
</template>
父组件:通过 v-slot=“slotProps” 接收子组件传过来的数据,自由定制 UI。
<template>
<UserList :users="userList">
<!-- 接收子组件传来的数据,这里用解构语法更简洁 -->
<template #userItem="{ user }">
<!-- 父组件完全控制了怎么渲染这个 user -->
<span style="color: red;">VIP用户:{{ user.name }}</span>
</template>
</UserList>
</template>
4. 注意事项
- 渲染作用域隔离:插槽内容是在父组件的模板里写的,所以它只能访问父组件的数据,不能直接访问子组件的数据(除非通过作用域插槽传出来)。
- 默认内容会被覆盖:只要父组件提供了插槽内容(哪怕里面是空的
<template #header></template>),子组件<slot>标签里的默认内容就绝对不会显示。 - Vue 3 语法统一:在 Vue 2 时代,具名插槽用 slot=“header”,作用域用 slot-scope。在 Vue 3 中,这些旧语法已被废弃,请全部统一使用 v-slot 及其简写 #。
- 独占默认插槽的缩写:如果组件只有默认插槽,你可以把 v-slot 直接写在组件标签上:
如果同时有默认插槽和具名插槽,默认插槽必须用<template #default>写,不能直接写在组件标签上。
<!-- 只有默认插槽时,可以这样简写 -->
<MyCard v-slot="{ data }">
<p>{{ data }}</p>
</MyCard>
二、页面卡片组件封装
实际场景:在后台管理系统中,我们通常不会把表格或表单直接裸露在页面上,而是用一个带有圆角、阴影的"卡片"把它们包裹起来。而且,卡片的标题旁边往往还需要放一些操作按钮(比如"新增"、“导出”)。
1. 封装子组件 (ContentWrap.vue)
子组件 ContentWrap.vue 负责提供卡片的整体骨架,通过 具名插槽 和 默认插槽 分别预留标题操作区和内容区,让父组件灵活定制。
<template>
<div class="content-wrap">
<!-- 头部区域:支持传入标题,并在右侧留出插槽 -->
<div class="wrap-header" v-if="title">
<span class="title">{{ title }}</span>
<!-- 具名插槽:父组件可以往这里塞按钮 -->
<div class="header-actions">
<slot name="header"></slot>
</div>
</div>
<!-- 默认插槽:包裹核心内容 -->
<div class="wrap-body">
<slot></slot>
</div>
</div>
</template>
<script setup>
defineProps({
title: String
})
</script>
2. 父组件使用
父组件通过 #header 具名插槽向标题右侧投放操作按钮,默认插槽则直接放入表格内容,实现卡片与业务逻辑的解耦。
<template>
<!-- 使用封装好的卡片,并传入标题 -->
<ContentWrap title="员工列表">
<!-- 通过具名插槽,自定义标题右侧的按钮 -->
<template #header>
<button @click="handleAdd">新增员工</button>
<button @click="handleExport">导出</button>
</template>
<!-- 默认插槽里放表格内容 -->
<table>
<tr><td>张三</td><td>前端</td></tr>
</table>
</ContentWrap>
</template>
三、数据表格组件封装
实际场景:企业里到处是表格。每个表格上面通常都有一个"工具栏"(左侧是标题,右侧是搜索框或刷新按钮)。如果每个页面都手写工具栏,代码会极其冗余。
1. 封装子组件 (ProTable.vue)
ProTable.vue 将工具栏和表格主体封装在一起,并通过 作用域插槽 把当前行数据 row 暴露给父组件,让父组件自由定制操作列的内容。
<template>
<div class="pro-table">
<!-- 工具栏区域 -->
<div class="toolbar">
<div class="left"><slot name="toolbar-left"></slot></div>
<div class="right"><slot name="toolbar-right"></slot></div>
</div>
<!-- 表格主体 -->
<table>
<thead><tr><th>姓名</th><th>年龄</th><th>操作</th></tr></thead>
<tbody>
<tr v-for="row in tableData" :key="row.id">
<td>{{ row.name }}</td>
<td>{{ row.age }}</td>
<td>
<!-- 作用域插槽:把当前行数据 row 传递给父组件 -->
<slot name="action" :row="row"></slot>
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script setup>
defineProps({
tableData: Array
})
</script>
关键点:<slot name="action" :row="row"> 这一行是作用域插槽的典型应用——子组件在遍历表格数据时,把每一行的 row 对象作为插槽 prop 抛出,父组件通过解构语法 { row } 拿到当前行数据后,即可自由渲染"编辑"、"删除"等操作按钮。
2. 父组件使用
父组件使用 ProTable 时,通过 #toolbar-left 和 #toolbar-right 这两个具名插槽分别定制工具栏左右两侧的内容,再通过 #action="{ row }" 作用域插槽获取当前行数据,实现行级操作。
<template>
<ProTable :tableData="userList">
<!-- 自定义工具栏左侧 -->
<template #toolbar-left>
<h3>用户管理</h3>
</template>
<!-- 自定义工具栏右侧 -->
<template #toolbar-right>
<input placeholder="搜索用户..." />
<button>刷新</button>
</template>
<!-- 自定义操作列:接收子组件传过来的 row 数据 -->
<template #action="{ row }">
<button @click="edit(row)">编辑</button>
<button @click="del(row.id)">删除</button>
</template>
</ProTable>
</template>

3067

被折叠的 条评论
为什么被折叠?



