Appearance
插槽
TIP
本小节对应的项目源代码,可以去 刘龙彬/vue3-component-study-3 仓库中下载。
插槽的基本概念

先举一个贴近生活的例子,好让大家更容易理解什么是插槽以及插槽的作用。
小霸王游戏机由“机身”和可插拔的游戏“卡带”组成,玩家在机身的卡槽中插入什么样的卡带,游戏机就在屏幕上渲染出什么样的游戏画面。在这个例子中,有 3 个非常重要的名词:机身、卡槽、卡带。其中:
- 卡槽是机身预留给用户的一个接口,至于游戏机运行时要渲染何种游戏,机身并不关心;
- 卡带是玩家提供的具体的游戏内容,把不同的卡带放入机身预留的插槽内,即可渲染出不同的游戏内容;
清楚了以上的例子之后,让我们再回到 Vue 的组件插槽(插槽的英文名叫做 Slot)。
我们在封装 SFC 组件的过程中,可以在 <template>
模板中预留一个内容的插槽,这个插槽中具体要渲染什么元素,将来由组件的使用者来动态提供。因此:
- 我们封装的这个组件,就相当于游戏机的“机身”;
- 我们在封装组件时预留的内容插槽,就相当于游戏机的“卡槽”;
- 组件的使用者提供的内容,就相当于游戏机的“卡带”;
总结一下:组件中的插槽,就是一个模板占位符。这个占位符具体要渲染什么内容,由将来组件的使用者来提供。

插槽的基本用法

封装 CustomButton
组件,通过 <slot>
标签来预留插槽占位符:
vue
<template>
<button>
<!-- 这里的 slot 标签,是封装组件时预留的“插槽” -->
<slot></slot>
</button>
</template>
在使用 <CustomButton>
组件时,可通过“内容区域”向预留的插槽中注入要渲染的“字符串内容/标签/其它组件”:
vue
<script setup>
import CustomButton from './components2/CustomButton.vue'
import TextContent from './components2/TextContent.vue'
</script>
<template>
<!-- 1. 向插槽提供“字符串内容” -->
<CustomButton>添加</CustomButton>
<!-- 2. 向插槽提供“标签内容” -->
<CustomButton>
<span>修改</span>
</CustomButton>
<!-- 3. 向插槽提供“组件内容” -->
<CustomButton>
<TextContent />
</CustomButton>
</template>
🚨 注意 🚨
如果子组件中没有声明 <slot>
插槽,则父组件传入的“插槽内容”会被丢弃!
默认内容
在组件中预留插槽时,还可以提供默认内容,仅当外部没有传入插槽内容的情况下,默认内容才会被渲染出来。语法格式如下:
html
<slot>默认内容</slot>
例如,在封装 CustomButton
组件时,向 <slot>
插槽提供默认内容:
vue
<template>
<button>
<!-- slot 开闭标签中间的内容,就是插槽的“默认内容” -->
<slot>按钮</slot>
</button>
</template>
🚨 注意 🚨
插槽的默认内容,也可以是“字符串/标签/组件”。
插槽命名
在一个组件中可以预留多个 slot 插槽,多个插槽之间可以使用 name
名称进行区分。
默认插槽
未提供名字的插槽叫做“默认插槽”:
html
<slot></slot>
默认插槽不是没有名字,而是名字被省略了,默认插槽的名字是 default
。完整的写法如下:
html
<slot name="default"></slot>
具名插槽
提供了 name
名称的插槽叫做“具名插槽”:
html
<slot name="header"></slot>
<slot></slot>
<slot name="footer"></slot>
在上面的例子中,共有两个具名插槽(header 和 footer),同时还有一个默认插槽。
v-slot 指令
v-slot
指令允许我们把不同的模板片段,根据插槽的名称放到对应的 <slot>
插槽中。其基本语法如下:
html
<template v-slot:插槽的名字>
<p>这个 p 元素要被渲染到指定名称的 slot 插槽中</p>
</template>
🚨 注意 🚨
v-slot
指令必须应用在 <template>
这个虚拟元素上,不能直接添加给实际的 DOM 节点。
例如,在下面的代码中,我们使用 v-slot:header
和 v-slot:footer
指令,分别向 header
和 footer
两个具名插槽中渲染了指定的模板片段:
html
<CustomLayout>
<template v-slot:header>
<h1>这是 Header 中的标题</h1>
</template>
<p>这是正文中的段落</p>
<p>这是正文中的段落</p>
<p>这是正文中的段落</p>
<template v-slot:footer>
<span>2024 © 刘龙宾</span>
<span> 京 ICP 备案</span>
</template>
</CustomLayout>
而正文的 3 个 p 段落会被渲染到默认插槽中,其完整写法如下:
html
<template v-slot:default>
<p>这是正文中的段落</p>
<p>这是正文中的段落</p>
<p>这是正文中的段落</p>
</template>
v-slot 指令的简写
v-slot 指令可以被简写为 #
,因此 <template v-slot:header>
可以简写为 <template #header>
。
下面是完整的、向 CustomLayout
传入插槽内容的代码,v-slot
指令均使用了 #
简写的形式:
html
<CustomLayout>
<template #header>
<h1>这是 Header 中的标题</h1>
</template>
<template #default>
<p>这是正文中的段落</p>
<p>这是正文中的段落</p>
<p>这是正文中的段落</p>
</template>
<template #footer>
<span>2024 © 刘龙宾</span>
<span> 京 ICP 备案</span>
</template>
</CustomLayout>
最终,组件渲染出来的 DOM 结构如下:
html
<div class="container">
<header>
<h1>这是 Header 中的标题</h1>
</header>
<main>
<p>这是正文中的段落</p>
<p>这是正文中的段落</p>
<p>这是正文中的段落</p>
</main>
<footer>
<span>2024 © 刘龙宾</span>
<span> 京 ICP 备案</span>
</footer>
</div>
条件插槽
在封装组件期间,如果我们不想写死固定个数的插槽占位符,而是想根据用户传入的插槽片段,来动态渲染插槽占位符。此时就需要用到模板内置的 $slots
变量和 v-if
指令。
例如,在之前的例子中,CustomLayout
组件的模板中定义了 header
,default
和 footer
这三个插槽。我们希望实现的效果是:如果用户没有传入 footer
对应的插槽内容,那么就不渲染 footer
对应的插槽占位符。修改后的代码如下:
vue
<template>
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<!-- 如果 $slots 对象中包含了用户传入的 footer 插槽的“渲染函数”,
则渲染 footer 标签以及嵌套的 slot 插槽;否则不渲染它们。 -->
<footer v-if="$slots.footer">
<slot name="footer"></slot>
</footer>
</div>
</template>
请注意:$slots.footer
是 footer
插槽的渲染函数,它本质是一个函数:
html
<!-- 浏览器渲染的结果是 function -->
<p>{{ typeof $slots.footer }}</p>
动态插槽名
v-slot:插槽名
指令中的插槽名部分支持动态变量,其语法格式如下:
html
<template v-slot:[插槽名的变量]> ... </template>
<!-- 上述代码简写如下: -->
<template #[插槽名的变量]> ... </template>
示例代码如下:
html
<script setup>
import CustomLayout from './components2/CustomLayout2.vue'
import { ref } from 'vue'
// 1. 响应式数据:动态插槽名
const slotName = ref('header')
</script>
<template>
<!-- 2. 点击按钮,切换插槽名变量的值 -->
<button class="btn-toggle" @click="slotName = 'footer'">切换 slotName 为 footer</button>
<CustomLayout>
<!-- 3. 根据“动态插槽名”,动态把模板片段渲染到指定的插槽中 -->
<template v-slot:[slotName]>
<h1>这是 Header 中的标题</h1>
</template>
<template v-slot:default>
<p>这是正文中的段落</p>
<p>这是正文中的段落</p>
<p>这是正文中的段落</p>
</template>
</CustomLayout>
</template>
<style scoped>
/* 按钮绝对定位到页面右上角 */
.btn-toggle {
position: absolute;
right: 0;
top: 0;
}
</style>
作用域插槽
作用域插槽指的是,在子组件中声明 <slot>
插槽的时候,通过 Props 向上传出数据,传出的数据可在父组件的“插槽内容”中接收并使用。
作用域插槽的语法格式如下:
html
<!-- 子组件 -->
<slot :text="content" :count="1"></slot>
在父组件中,可以通过 v-slot="变量名"
来接收子组件的作用域插槽传出的数据:
html
<template v-slot="slotProps">
<p>{{ slotProps.text }}</p>
<p>{{ slotProps.count }}</p>
</template>
组件的 Props vs. 插槽上的 Props
组件上的 Props 负责向下传入数据,插槽上的 Props 负责向上传出数据。两者的图示如下:
具名作用域插槽
每个具名插槽也可以通过 props
向上传出数据,父组件可以分别通过 v-slot:插槽名称="slotProps"
来接收传出的数据。
简写形式为 #插槽名称="slotProps"
。
我们在子组件中声明具名的作用域插槽,示例代码如下:
vue
<!-- 子组件 -->
<script setup>
import { ref } from 'vue'
// 响应式数据
const article = ref({
title: '滕王阁序',
content: ['豫章故郡,洪都新府。', '星分翼轸,地接衡庐。', '襟三江而带五湖,控蛮荆而引瓯越。'],
author: '王勃'
})
</script>
<template>
<div class="container">
<header>
<!-- 具名作用域插槽 -->
<slot name="header" :title="article.title"></slot>
</header>
<main>
<!-- 默认作用域插槽 -->
<slot :content="article.content"></slot>
</main>
<footer v-if="$slots.footer">
<!-- 具名作用域插槽 -->
<slot name="footer" :author="article.author"></slot>
</footer>
</div>
</template>
<style scoped>
.container {
display: flex;
flex-direction: column;
height: 100%;
}
header {
height: 60px;
background-color: #2b4b6b;
color: #fff;
}
main {
flex: 1;
background-color: #fff;
}
footer {
height: 50px;
background-color: lightgray;
font-size: 12px;
display: flex;
justify-content: center;
align-items: center;
}
</style>
在父组件中接收作用域插槽传出的数据:
vue
<script setup>
import CustomLayout from './components2/CustomLayout2.vue'
import { ref } from 'vue'
// 1. 响应式数据:动态插槽名
const slotName = ref('header')
</script>
<template>
<!-- 2. 点击按钮,切换插槽名变量的值 -->
<button class="btn-toggle" @click="slotName = 'footer'">切换 slotName 为 footer</button>
<CustomLayout>
<!-- 3. 根据“动态插槽名”,动态把模板片段渲染到指定的插槽中 -->
<template #[slotName]="slotProps">
<!-- 按需渲染对应的标签 -->
<h1 v-if="slotName === 'header'">{{ slotProps.title }}</h1>
<span v-else>作者:{{ slotProps.author }}</span>
</template>
<template v-slot:default="slotProps">
<p v-for="row in slotProps.content" :key="row">{{ row }}</p>
</template>
</CustomLayout>
</template>
<style scoped>
/* 按钮绝对定位到页面右上角 */
.btn-toggle {
position: absolute;
right: 0;
top: 0;
}
</style>