Vue3的slot用法怎么学透?基础、进阶到实战一次讲明白
Vue3里的slot是干什么用的?
简单说,slot(插槽)是Vue里实现分发的工具,想象你做了个“快递盒子”组件,盒子负责样式、布局(比如边框、背景),但盒子里放啥(是放文件、鲜花还是玩具)由用这个组件的父组件决定——slot就是盒子里的“空位”,专门用来装父组件想塞的内容。
举个实际开发场景:做弹窗组件时,不同页面的弹窗里可能要放表单、按钮组、图片等不同内容,如果没有slot,得给弹窗组件写N个props传内容,还得处理HTML结构,特别麻烦,有了slot,弹窗组件里留个<slot>,父组件直接在<弹窗组件>标签里写内容,弹窗负责把内容“嵌”到正确位置,灵活又省心。
怎么用默认插槽?最简单的场景长啥样?
默认插槽是没有名字的插槽,子组件里写个<slot>占位,父组件在子组件标签里直接塞内容就行。
基础用法示例
子组件(MyButton.vue)负责按钮的样式,内容让父组件定:
<template>
<button class="custom-btn">
<slot></slot> <!-- 这里是默认插槽,父组件传的内容会替换到这 -->
</button>
</template>
<style scoped>
.custom-btn { padding: 8px 16px; border: 1px solid #ccc; }
</style>
父组件用的时候,直接在<MyButton>标签里写内容:
<template> <MyButton>点击提交</MyButton> <!-- “点击提交”会被放到子组件的slot位置 --> </template>
给默认插槽加“兜底内容”
如果父组件没传内容,想显示默认文字,子组件的<slot>里可以写默认内容:
<slot>默认按钮</slot> <!-- 父组件没传内容时,显示“默认按钮”;传了就显示父组件的内容 -->
具名插槽在Vue3里怎么写?和Vue2有啥不一样?
当子组件需要多个不同位置的插槽时,就得给slot起名字(具名插槽),比如布局组件要分“头部、主体、底部”三个区域,每个区域用不同名字的slot。
Vue3的具名插槽写法
子组件(Layout.vue)里给slot加name属性:
<template>
<div class="layout">
<header><slot name="header"></slot></header>
<main><slot name="main"></slot></main>
<footer><slot name="footer"></slot></footer>
</div>
</template>
父组件用的时候,得用<template>配合v-slot:名字(或缩写#名字)来指定内容该往哪个slot里塞:
<Layout>
<template #header> <!-- #header 是 v-slot:header 的缩写 -->
<h1>页面大标题</h1>
</template>
<template #main>
<p>这里是正文内容...</p>
</template>
<template #footer>
<p>©2024 版权信息</p>
</template>
</Layout>
和Vue2的区别
Vue2里是直接在标签上加slot="名字",
<!-- Vue2 写法,现在Vue3不推荐了 --> <header slot="header">标题</header>
Vue3把具名插槽的语法统一到<template>的v-slot上,避免了“普通标签加slot属性可能混淆作用域”的问题,写法更规范,也更易维护。
作用域插槽怎么理解?实际开发哪里能用到?
作用域插槽是子组件给插槽传数据,父组件用的时候能拿到这些数据,比如子组件是个“Todo列表”,每个列表项的结构父组件想自定义,但数据(比如每个Todo的文字、是否完成)在子组件里——这时候作用域插槽就能让子组件把数据“传给”父组件的插槽内容。
代码示例:Todo列表自定义渲染
子组件(TodoList.vue)里,循环渲染Todo时,把每个todo数据传给slot:
<template>
<ul class="todo-list">
<li v-for="todo in todos" :key="todo.id">
<slot :todo="todo"></slot> <!-- 把todo对象传给插槽,父组件能拿到 -->
</li>
</ul>
</template>
<script setup>
import { ref } from 'vue'
const todos = ref([
{ id: 1, text: '吃饭', done: false },
{ id: 2, text: '睡觉', done: true }
])
</script>
父组件用的时候,通过v-slot="接收的数据"拿到子组件传的todo:
<TodoList>
<template #default="slotProps"> <!-- #default 对应默认插槽,也可以省略不写 -->
<input type="checkbox" :checked="slotProps.todo.done" />
{{ slotProps.todo.text }}
</template>
</TodoList>
觉得slotProps太长?还能解构赋值简化:
<template #default="{ todo }"> <!-- 直接把slotProps里的todo解构出来 -->
<input type="checkbox" :checked="todo.done" />
{{ todo.text }}
</template>
实际开发场景
- 表格组件:列的内容(比如操作按钮、带tooltip的文字)由父组件定,但行数据在子组件里,用作用域插槽传每行数据;
- 下拉选择器:选项的显示样式(比如带图标、带标签)由父组件定,选项数据在子组件里,用作用域插槽传选项数据;
- 卡片组件:卡片底部的操作按钮由父组件定,但卡片数据(比如标题、描述)在子组件里,用作用域插槽传卡片数据。
插槽和props、emit有啥不同?能互相替代吗?
props、emit、slot是Vue组件通信的三种方式,场景完全不同,没法互相替代:
| 方式 | 作用 | 传递的是… | 典型场景 |
|---|---|---|---|
| props | 父组件→子组件传数据 | 纯数据(字符串、对象等) | 给子组件传标题、配置项等 |
| emit | 子组件→父组件触发事件 | 事件(带可选参数) | 子组件点击按钮,通知父组件处理 |
| slot | 父组件→子组件传“结构/内容” | HTML结构、组件组合 | 给弹窗传表单、给列表传自定义项 |
举个“弹窗组件”的例子更清楚: 用props传:<Popup title="提示" />;
- 弹窗关闭时用emit通知父组件:
<button @click="$emit('close')">关闭</button>; - 弹窗里的表单内容用slot传:
<Popup><Form /></Popup>。
所以三者是互补关系,根据需求选合适的方式就行。
在Composition API组件里用slot要注意什么?
Vue3的Composition API(比如<script setup>)写组件时,用slot和选项式API差别不大,但有个实用技巧:用useSlots()判断插槽是否存在。
子组件里判断插槽是否存在
比如做个布局组件,想根据“是否有header插槽”来决定是否渲染头部区域:
<template>
<div class="layout-comp">
<div class="header" v-if="hasHeader">
<slot name="header"></slot>
</div>
<div class="content">
<slot></slot>
</div>
</div>
</template>
<script setup>
import { useSlots } from 'vue' // 导入useSlots
const slots = useSlots() // 获取所有插槽
const hasHeader = !!slots.header // 判断header插槽是否存在
</script>
这样父组件没传header插槽时,子组件就不渲染头部区域,避免空标签占位影响样式。
插槽嵌套、动态插槽名这些进阶玩法怎么实现?
实际开发中,插槽不止用一层,还能玩“动态切换插槽名”,这俩技巧能让组件灵活性拉满。
插槽嵌套:父组件传的内容里再放插槽
比如做个“按钮包装器”组件(ButtonWrapper),负责按钮的样式,而按钮里的图标由更上层的父组件(祖父组件)决定:
-
子组件(ButtonWrapper.vue):
<template> <div class="btn-wrapper"> <slot></slot> <!-- 接收父组件传的按钮 --> </div> </template>
-
父组件(用ButtonWrapper时,给按钮里加slot):
<ButtonWrapper> <MyButton> <slot name="icon"></slot> <!-- 这个slot由祖父组件传 --> 点击提交 </MyButton> </ButtonWrapper>
-
祖父组件(给icon插槽传内容):
<ButtonWrapper> <template #icon> <Icon name="plus"></Icon> <!-- 自定义图标 --> </template> </ButtonWrapper>
动态插槽名:插槽名由变量控制
比如做主题切换组件,不同主题下要渲染不同名字的插槽,Vue3里用v-slot:[变量名](或缩写#[变量名])实现:
父组件里,插槽名由变量slotName控制:
<template>
<ThemeComponent>
<template v-slot:[slotName]> <!-- 动态绑定插槽名 -->
这是{{ slotName }}插槽的内容
</template>
</ThemeComponent>
</template>
<script setup>
import { ref } from 'vue'
const slotName = ref('dark-content') // 变量控制插槽名
</script>
子组件(ThemeComponent.vue)里对应多个插槽:
<template>
<div class="theme">
<slot name="dark-content"></slot>
<slot name="light-content"></slot>
</div>
</template>
当slotName变成light-content时,就会渲染light-content对应的插槽内容,适合动态切换布局、主题的场景。
开发UI组件库时,slot有哪些常见设计思路?
做UI组件库时,插槽是让组件“可扩展”的核心武器,常见设计思路有这几种:
布局型组件:多插槽拆分区域
比如Layout组件分header、aside、main、footer,用户能自由组合页面结构:
<template>
<div class="layout">
<header><slot name="header"></slot></header>
<aside><slot name="aside"></slot></aside>
<main><slot name="main"></slot></main>
<footer><slot name="footer"></slot></footer>
</div>
</template>
用户用的时候,想怎么布局就怎么传内容,组件库只负责提供“区域容器”。
功能型组件:留扩展插槽
比如Table组件,列头、单元格、页脚都留插槽,让用户自定义:
<template>
<table>
<thead>
<tr>
<th v-for="col in columns" :key="col.key">
<slot name="header" :col="col">{{ col.title }}</slot> <!-- 列头默认显示标题,用户可自定义 -->
</th>
</tr>
</thead>
<tbody>
<tr v-for="row in data" :key="row.id">
<td v-for="col in columns" :key="col.key">
<slot name="cell" :row="row" :col="col">{{ row[col.key] }}</slot> <!-- 单元格默认显示数据,用户可自定义 -->
</td>
</tr>
</tbody>
<tfoot>
<slot name="footer"></slot> <!-- 页脚让用户自定义分页、汇总等 -->
</tfoot>
</table>
</template>
用户可以给列头加筛选按钮,给单元格加编辑图标,自由度拉满。
状态型组件:内容插槽+兜底
比如Empty组件(空状态),默认显示“暂无数据”,用户也能传自定义内容:
<template>
<div class="empty">
<slot>
<img src="@/assets/empty.png" />
<p>暂无数据</p>
</slot>
</div>
</template>
时显示默认空状态,传了就显示用户内容,兼顾通用性和扩展性。
用slot容易踩的坑有哪些?怎么避坑?
插槽看似简单,实际开发稍不注意就会踩坑,这几个常见问题要警惕:
不渲染:检查slot和传值位置
- 子组件没写
<slot>:再好的内容没地方放也白搭,记得子组件里留<slot>占位; - 具名插槽没写对
name:父组件<template #name>的name要和子组件<slot name="name">的name完全一致,拼写错一个字母都不行; - 默认插槽传内容时,父组件直接写标签里,不用包
<template>(除非和具名插槽混用)。
作用域插槽拿不到数据:检查数据传递和解构
- 子组件没给slot传数据:比如要传
todo="todo",漏写v-bind就传不过去; - 父组件解构名字错了:子组件传的是
item="item",父组件用{ list }接收,名字对不上就拿不到。
Vue2和Vue3语法混淆:牢记Vue3规则
Vue3里具名插槽必须用<template #name>,不能像Vue2那样在普通标签上加slot="name",新项目直接用Vue3语法,旧项目迁移时全局替换。
插槽嵌套导致作用域丢失:中间层要透传数据
比如祖父组件给父组件传slot带数据,父组件给子组件传的时候,得把接收到的slotProps再通过v-bind传给子组件的slot:
父组件(中间层)接收并透传数据:
<ChildComponent>
<template #default="slotProps">
<GrandChildComponent>
<slot :data="slotProps.data"></slot> <!-- 把数据透传给子组件的slot -->
</GrandChildComponent>
</template>
</ChildComponent>
Vue3插槽和React的Children有什么区别?
虽然Vue的slot和React的Children都解决“组件内容自定义”问题,但实现思路不一样:
- Vue的slot是“显式占位”:子组件用
<slot>明确告诉父组件“我这里要插内容”,父组件用v-slot明确告诉子组件“我要插在这个位置”,还能通过作用域插槽传数据,语法糖多,写起来直观; - React的Children是“隐式传递”:子组件通过
this.props.children获取父组件传的内容,要实现“子传父数据”(类似Vue作用域插槽),得用render props模式(子组件传个函数,父组件调用函数传数据)。
举个“自定义列表项”的例子对比:
Vue(作用域插槽):
<Child>
<template #default="{ item }">{{ item.name }}</template>
</Child>
React(render props):
<Child render={(item) => <div>{item.name}</div>} />
Vue的slot更像“预先留好位置等内容”,React的Children更像“把内容当参数传递”,核心都是让组件更灵活,但Vue的语法对新手更友好,React的写法更偏函数式编程。
写在最后:
slot是Vue组件化开发里“让组件既复用又灵活”的关键,从最基础的默认插槽,到能传数据的作用域插槽,再到组件库级别的插槽设计,掌握这些用法后,写组件时再也不用纠结“内容怎么让用户自定义”,多在项目里练手(比如封装个带插槽的表格、弹窗),自然能吃透slot的精髓~
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网


