Vue3里slot default怎么用?从基础到实战一次讲透
默认插槽(slot default)到底是什么?
简单说,默认插槽是Vue组件里没给插槽起名字的那类插槽,你可以把组件想象成一个“容器模板”,插槽就是容器里预留的“空位”——父组件往这个空位里塞内容,子组件负责把这些内容渲染到指定位置。
和“具名插槽”对比着看更清楚:具名插槽是给<slot>加了name属性(比如<slot name="header"></slot>),父组件得用<template #header>这种方式对应;而默认插槽没name,父组件直接在子组件标签里写内容会自动跑到子组件的<slot></slot>位置里。
举个生活例子:你点外卖时,商家给的餐盒是“子组件”,餐盒上有个“默认放饭菜的格子”(对应default slot),你把点的菜(父组件内容)放进这个格子,商家拿到后直接把格子里的菜放进餐盒——不用给这个格子贴标签,因为它是“默认放主菜”的位置。
怎么在父组件和子组件之间用default slot传内容?
这得分成“子组件定义插槽”和“父组件传递内容”两步来做:
子组件里定义default slot
子组件的模板中,只要写一个<slot></slot>标签,就相当于“挖了个默认的坑”,比如做个卡片组件MyCard:
<template>
<div class="card">
<div class="card-header">固定的头部</div>
<slot></slot> <!-- 这里是默认插槽的位置 -->
<div class="card-footer">固定的底部</div>
</div>
</template>
这个<slot></slot>就是父组件内容要插入的地方,如果子组件没写<slot>,父组件往子组件标签里塞的内容根本没地方放,最终页面上不会显示。
父组件往default slot传内容
父组件用子组件时,直接在子组件标签内部写内容,这些内容会自动“填”到子组件的<slot></slot>位置,比如父组件用MyCard:
<template>
<MyCard>
<!-- 这里所有内容都会进MyCard的default slot -->
<h2>这是卡片标题</h2>
<p>这是卡片正文,能自定义任何结构</p>
<button>卡片里的按钮</button>
</MyCard>
</template>
渲染后,MyCard里的<slot></slot>会被替换成父组件写的<h2>、<p>、<button>,最终页面上卡片的头部、底部是固定的,中间内容是父组件传的自定义内容。
default slot和具名插槽核心区别是啥?
最直观的区别是有没有“名字”,但实际用的时候差异更明显:
| 对比项 | 默认插槽(default slot) | 具名插槽(named slot) |
|---|---|---|
| 插槽定义 | 子组件写<slot></slot>(无name) |
子组件写<slot name="xxx"></slot> |
| 适用场景 | 组件只有一个“主要自定义区域” | 组件有多个“分区域自定义”需求 |
举个实际例子:做一个页面布局组件PageFrame,需要头部、主体、底部三个可自定义区域,如果主体是“默认区域”,头部和底部是“具名区域”,代码会这样写:
子组件PageFrame:
<template>
<div class="page">
<header>
<slot name="header"></slot> <!-- 具名插槽header -->
</header>
<main>
<slot></slot> <!-- 默认插槽,对应主体 -->
</main>
<footer>
<slot name="footer"></slot> <!-- 具名插槽footer -->
</footer>
</div>
</template>
父组件用PageFrame时:
<PageFrame>
<!-- 具名插槽header的内容,必须用<template #header>包起来 -->
<template #header>
<h1>网站标题</h1>
</template>
<!-- 默认插槽的内容,直接写,不用包template -->
<p>这是页面主体内容,自动进main里的default slot</p>
<!-- 具名插槽footer的内容,用<template #footer>包 -->
<template #footer>
<p>©2024 版权信息</p>
</template>
</PageFrame>
能看出来:默认插槽不用写<template #xxx>更“直接”;具名插槽必须用<template>配合name,才能精准对应子组件的命名插槽。
作用域插槽在default里咋玩?
作用域插槽是Vue里“子组件给父组件传数据”的关键玩法——子组件可以把自己的数据,通过<slot>传递给父组件,让父组件基于子组件数据自定义渲染内容,而default slot也能玩这套逻辑~
步骤分两步:子组件传数据给slot → 父组件用v-slot接收数据。
子组件:给default slot绑数据
子组件在<slot>上通过v-bind(或简写)传递数据,比如做个用户信息组件UserProfile,要把用户对象传给父组件:
<template>
<div class="user-box">
<!-- 给default slot传user数据 -->
<slot :user="user"></slot>
</div>
</template>
<script setup>
import { ref } from 'vue'
const user = ref({ name: '阿花', age: 25, job: '设计师' })
</script>
父组件:用v-slot接收数据
父组件在子组件标签上用v-slot指令(或简写),捕获子组件传的内容,比如父组件要自定义用户信息的展示:
<template>
<UserProfile v-slot="scope">
<!-- scope是个对象,包含子组件传的user -->
<p>姓名:{{ scope.user.name }}</p>
<p>年龄:{{ scope.user.age }}</p>
<p>职业:{{ scope.user.job }}</p>
</UserProfile>
</template>
如果觉得scope写起来麻烦,还能结构赋值:
<UserProfile v-slot="{ user }">
<p>姓名:{{ user.name }}</p>
<p>年龄:{{ user.age }}</p>
</UserProfile>
这样父组件既能拿到子组件的user数据,又能自由决定怎么渲染这些数据——而这一切,都是通过default slot完成的~
实战中default slot能解决哪些真实场景?
默认插槽的灵活性,让它在“组件需要一个主要自定义区域”的场景里特别好用,分享三个常见场景:
场景1:通用布局组件
很多项目需要统一的页面结构(比如头部导航、底部版权、中间主体),用default slot做“中间主体”的自定义区域,能让代码更简洁:
子组件BasicLayout:
<template>
<div class="layout">
<nav class="nav">
<slot name="nav"></slot> <!-- 具名插槽:导航 -->
</nav>
<section class="main">
<slot></slot> <!-- 默认插槽:主体内容 -->
</section>
<footer class="footer">
<slot name="footer"></slot> <!-- 具名插槽:页脚 -->
</footer>
</div>
</template>
父组件用BasicLayout时,主体内容直接写,不用给slot起名字:
<BasicLayout>
<template #nav>
<a href="/">首页</a>
<a href="/about">lt;/a>
</template>
<!-- 主体内容直接写,进default slot -->
<article>
<h2>文章标题</h2>
<p>文章正文...</p>
</article>
<template #footer>
<p>联系我们:xxx@xxx.com</p>
</template>
</BasicLayout>
场景2:可自定义的弹窗组件
弹窗(Dialog)组件通常需要“弹窗内容”和“按钮操作区”两个自定义区域,把“弹窗内容”作为default slot,“按钮区”作为具名插槽,用起来更顺手:
子组件Dialog:
<template>
<div class="dialog-mask">
<div class="dialog-box">
<div class="dialog-content">
<slot></slot> <!-- 默认插槽:弹窗内容 -->
</div>
<div class="dialog-footer">
<slot name="footer"></slot> <!-- 具名插槽:按钮区 -->
</div>
</div>
</div>
</template>
父组件调用Dialog时,弹窗内容直接写,按钮区用具名插槽:
<Dialog>
<!-- 弹窗内容进default slot -->
<p>确认删除这条数据吗?</p>
<p>删除后无法恢复哦~</p>
<!-- 按钮区进具名插槽footer -->
<template #footer>
<button @click="cancel">取消</button>
<button @click="confirm">确认</button>
</template>
</Dialog>
场景3:动态列表渲染
做列表组件时,往往需要“父组件自定义列表项的UI”(比如有的列表显示文字,有的显示卡片),用default slot结合作用域插槽,能完美实现:
子组件MyList:
<template>
<ul class="list">
<li v-for="item in list" :key="item.id">
<!-- 给每个列表项的default slot传item数据 -->
<slot :item="item"></slot>
</li>
</ul>
</template>
<script setup>
const props = defineProps(['list'])
</script>
父组件用MyList时,自定义每个列表项的显示:
<template>
<MyList :list="taskList">
<!-- 用v-slot接收每个item -->
<template v-slot="{ item }">
<div class="task-card">
<h3>{{ item.title }}</h3>
<p>{{ item.desc }}</p>
<span>{{ item.status }}</span>
</div>
</template>
</MyList>
</template>
<script setup>
import { ref } from 'vue'
const taskList = ref([
{ id: 1, title: '任务1', desc: '...', status: '进行中' },
{ id: 2, title: '任务2', desc: '...', status: '已完成' }
])
</script>
这里子组件循环渲染列表项,每个项的内容由父组件通过default slot自定义——既保留了列表的结构逻辑(子组件负责循环),又让UI完全可定制(父组件负责渲染)。
default slot的进阶玩法有哪些?
除了基础用法,默认插槽还有些“隐藏技巧”能提升开发效率:
技巧1:Fallback内容(兜底内容)
子组件的<slot>里可以写“默认内容”——如果父组件没传内容,就显示这个默认内容;父组件传了内容,就替换掉默认内容。
比如做个评论组件CommentBox,没人评论时显示提示:
<template>
<div class="comment-list">
<slot>
<p>还没有人发表评论~</p> <!-- fallback内容 -->
</slot>
</div>
</template>
时,页面显示“还没有人发表评论~”;如果父组件传了评论列表:
<CommentBox>
<div v-for="comment in comments" :key="comment.id">
<p>{{ comment.author }}:{{ comment.content }}</p>
</div>
</CommentBox>
这时子组件的fallback内容会被替换,显示真实评论。
技巧2:在slot里用v-if/v-for
不管是父组件传的内容,还是子组件slot里的fallback内容,都能正常用Vue的指令。
比如父组件给default slot传一个带v-for的列表:
<MyComponent>
<ul>
<li v-for="item in menuList" :key="item.id">{{ item.name }}</li>
</ul>
</MyComponent>
子组件的slot里也能包条件渲染:
<template>
<div class="box">
<slot>
<!-- fallback内容里用v-if -->
<p v-if="showTip">请先添加内容</p>
</slot>
</div>
</template>
<script setup>
const showTip = ref(true)
</script>
技巧3:嵌套插槽(谨慎使用)
理论上,default slot里还能嵌套其他组件的插槽,但这种情况容易让代码复杂度飙升,除非是极复杂的组件嵌套,否则不建议。
子组件A里的default slot,又包含子组件B的default slot:
<template>
<div class="a">
<slot>
<!-- 子组件A的default slot里嵌套子组件B -->
<B>
<slot></slot> <!-- 子组件B的default slot -->
</B>
</slot>
</div>
</template>
这种写法会让插槽的层级和数据传递变得很绕,维护成本高,如果不是必须,优先用“单一插槽职责”设计组件。
遇到default slot不生效咋排查?
开发中遇到“父组件传的内容没显示”,可以从这几个方向排查:
原因1:子组件没写<slot>
子组件的模板里如果没有<slot></slot>,父组件往子组件标签里塞的内容没有挂载点,自然不会显示,打开子组件文件,检查是否有<slot>标签。
原因2:作用域插槽数据没接对
如果用了作用域插槽(子组件传数据给父组件),要检查:
- 子组件
<slot>上的v-bind变量名是否正确(比如子传user,父组件v-slot里要拿user); - 父组件
v-slot的接收语法是否正确(比如v-slot="scope",scope里有没有对应的字段)。
举个错误例子:子组件传的是user,父组件写成v-slot="{ username }",这时候username是undefined就会不显示。
原因3:插槽被条件渲染覆盖
子组件里的<slot>如果被v-if包着,且条件不满足,slot就不会渲染,父组件内容也跟着消失。
<template>
<div>
<slot v-if="isShow"></slot> <!-- isShow为false时,slot不渲染 -->
</div>
</template>
检查子组件slot的渲染条件是否正确。
原因4:多个default slot导致冲突
子组件里如果写了多个<slot></slot>(没有name的那种),父组件传的内容会被每个slot重复渲染,或者因为插槽合并逻辑导致显示异常,Vue官方建议:一个子组件里只写一个default slot,避免混淆。
default slot是Vue3插槽体系里“最灵活、最基础”的存在——它让父组件能以最简洁的方式给子组件传内容,同时结合作用域插槽、fallback内容等特性,能覆盖从简单到复杂的组件通信场景,掌握它的关键是理解“匿名插槽的定位”+“父子组件的配合逻辑”+“实战场景的灵活运用”,多写几个例子,自然就熟了~
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网



