vue3 内置特殊元素<slot> 与 插槽 Slots
vue官网
内置特殊元素<slot>
插槽 Slots
<slot>
<slot>
元素是一个插槽出口 (slot outlet),标示了父元素提供的插槽内容 (slot content) 将在哪里被渲染。
- Vue 模板里的
<slot>
元素会被编译到 JavaScript,因此不要与原生<slot>
元素进行混淆。 <slot>
元素可以使用name
attribute 来指定插槽名。- 当没有指定
name
或者name="default"
时,将会渲染默认插槽。 - 传递给插槽元素的附加 attributes 将作为插槽 props,传递给父级中定义的作用域插槽。
- 示例:
- 当没有指定
<!-- 默认插槽 -->
<slot></slot><!-- 名为content的插槽 -->
<slot name="content"></slot><!-- title/message将作为插槽props 传递给父组件 -->
<slot name="content" title="内容" message="插槽内容"></slot>
- 元素本身将被其所匹配的插槽内容替换。
如下图所示,使用<slot>
作为一个占位符,父组件传递进来的内容就会渲染在这里。
假设有一个带有插槽的<Message>
组件:
<!-- 带有插槽的Message组件 -->
<template><div class="msg"><!-- 默认插槽 --><slot></slot></div>
</template>
使用<Message>
组件的插槽:
<template><Message>Error!</Message>
</template>
最终渲染出的 DOM 如下:
<div class="msg">Error!</div>
通过使用插槽,<Message>
仅负责渲染外层的 <div>
(以及相应的样式),而其内部的内容由父组件提供。
插槽内容可以是任意合法的模板内容,不局限于文本。可以传入多个元素,甚至是组件。
<Message><Icon name="icon-error" /><span>Error!</span>
</Message>
为插槽指定默认内容
在外部没有提供任何内容的情况下,可以为插槽指定默认内容。
默认内容写在 <slot>
标签之间。
<!-- Button.vue -->
<button class="btn-default"><slot>确认 <!-- 默认内容 --></slot>
</button>
按钮内部包含了一个<slot>
元素,它作为一个占位符,用于接收父组件传递过来的内容。
如果父组件在使用这个按钮组件时没有向插槽传递任何内容,那么插槽中将显示默认的文本 “确认”。
现在,当在父组件中使用 <Button>
且没有提供任何插槽内容时:
<Button />
“确认” 将会被作为默认内容渲染:
<button class="btn-default">确认</button>
如果提供了插槽内容:
<Button>取消</Button>
那么被显式提供的内容会取代默认内容:
<button class="btn-default">取消</button>
默认插槽
当子组件中没有指定具名插槽时,父组件传入的内容会被渲染到默认插槽中。
如果子组件中只有一个未命名的<slot>
元素,它就是默认插槽:
<template><div><h3>子组件</h3><!-- 默认插槽 --><slot></slot></div>
</template>
父组件中可以直接在子组件标签内部传递内容,这些内容将被插入到子组件的默认插槽中:
<template><div><ChildComponent><p>这是传递给子组件默认插槽的内容</p></ChildComponent></div>
</template>
具名插槽
<slot>
元素可以有一个特殊的 attribute name
,用来给各个插槽分配唯一的 ID,以确定每一处要渲染的内容。
显式的带 name
的<slot>
被称为具名插槽 (named slots)。没有提供 name
的 <slot>
出口会隐式地命名为“default
”。
子组件<Layout>
组件的模板如下:
<!-- 子组件 -->
<template><div><header><slot name="header"></slot></header><main><!-- 默认插槽,name为default --><slot></slot></main><footer><slot name="footer"></slot></footer></div>
</template>
在父组件中使用<Layout>
时,使用具名插槽将多个插槽内容传入到各自目标插槽的出口:
<Layout><template v-slot:header><!-- header 插槽的内容放这里 --></template><!-- # 是 v-slot 的简写 --><template #header><!-- header 插槽的内容放这里 --></template>
</Layout>
要为具名插槽传入内容,使用一个含 v-slot
指令的 <template>
元素,并将目标插槽的名字传给该指令:
v-slot:header
是一种明确指定插槽名称的方式,在<template>
标签内部放置要插入到子组件header
插槽的内容。#header
是v-slot:header
的简写形式,同样用于指定header
插槽,并在<template>
标签内部放置插槽内容。
在父组件中,完整的使用<Layout>
组件:
<Layout><template #header><h1>这是头部内容</h1></template><template #default><p>这是主体内容</p></template><template #footer><p>这是底部内容</p></template>
</Layout>
当一个组件同时接收默认插槽和具名插槽时,所有位于顶级的非 <template>
节点都被隐式地视为默认插槽的内容。所以上面也可以写成:
<Layout><template #header><h1>这是头部内容</h1></template><!-- 隐式的默认插槽 --><p>这是主体内容</p><template #footer><p>这是底部内容</p></template>
</Layout>
父组件通过指定插槽的名称,向不同的插槽传递了不同的内容。
条件插槽
条件插槽:根据特定条件来决定是否渲染某个插槽内容。
- 可以在父组件传递给子组件的插槽内容中使用
v-if
或v-show
指令来根据条件控制插槽内容的显示。
<Layout><template #header><h1 v-show="showHeader">这是头部内容</h1><h1 v-if="showHeader">这是头部内容</h1></template>
</Layout>
<script setup lang="ts">
import { ref } from 'vue'
const showHeader = ref(true)
</script>
- 结合使用
$slots
属性与v-if
来实现
子组件<Card>
的模板内容如下:
<template><div><div v-if="$slots.title" class="card-title"><slot name="title" /></div><div v-if="$slots.content" class="card-content"><slot name="content" /></div></div>
</template>
定义了两个具名插槽 title
和 content
,用于接收父组件传递的特定内容,并在相应的位置进行渲染。
通过使用v-if="$slots.title"
和v-if="$slots.content"
分别判断是否存在名为 title
和 content
的插槽内容。只有当对应的插槽有内容时,才会渲染包含该插槽的<div>
元素。
在父组件中使用子组件<Card>
:
<Card><template #title><h1>这是Card的标题</h1></template>
</Card>
渲染结果如下:
<div><div class="card-title"><h1>这是Card的标题</h1></div>
</div>
动态插槽名
动态指令参数在 v-slot
上也是有效的,即可以定义下面这样的动态插槽名:
<Layout><template v-slot:[dynamicSlotName]>...</template><!-- 缩写为 --><template #[dynamicSlotName]>...</template>
</Layout>
渲染作用域(传统插槽)
传统插槽是父组件向子组件传递插槽内容(父传子)。
插槽内容可以访问到父组件的数据作用域,因为插槽内容本身是在父组件模板中定义的。
<span>{{ ErrorMsg }}</span>
<Message> {{ ErrorMsg }} </Message>
这里的两个 {{ ErrorMsg }}
插值表达式渲染的内容都是一样的。
插槽内容无法访问子组件的数据。Vue 模板中的表达式只能访问其定义时所处的作用域,这和 JavaScript 的词法作用域规则是一致的:
- 父组件模板中的表达式只能访问父组件的作用域。
- 子组件模板中的表达式只能访问子组件的作用域。
作用域插槽
基本概念:
- 传统插槽(非作用域插槽,父传子):父组件向子组件传递静态内容,子组件在特定位置渲染这些内容。父组件无法直接访问子组件内部的数据来动态决定插槽内容的渲染方式。
- 作用域插槽(子传父):子组件可以将数据暴露给父组件,父组件在使用插槽时可以通过解构赋值等方式获取这些数据,并根据数据动态地渲染插槽内容。
默认作用域插槽
子组件在渲染时将数据提供给插槽:可以像对组件传递 props 那样,向一个插槽的出口上传递 attributes。
<!-- <MyComponent> 的模板 -->
<div><slot :text="greetingMessage" :count="1"></slot>
</div>
默认插槽通过子组件标签上的 v-slot
指令,直接接收到了一个插槽 props
对象:
<MyComponent v-slot="slotProps">{{ slotProps.text }} {{ slotProps.count }}
</MyComponent>
子组件传入插槽的 props 作为了 v-slot
指令的值,可以在插槽内的表达式中访问。
在父组件的模板中,也可以通过解构赋值获取子组件传递的数据:
<MyComponent v-slot="{ text, count }">{{ text }} {{ count }}
</MyComponent>
具名作用域插槽
具名作用域插槽 的 props 可以作为 v-slot
指令的值被访问到,写法如下:
v-slot:name="slotProps"
- 简写方式:
#name=slotProps
- 可以通过解构赋值获取子组件传递的数据:
v-slot:name="{ param1, param2 }"
或#name="{ param1, param2 }"
<!-- <MyComponent> 的模板 -->
<div><slot name="header" :text="greetingMessage" :count="1"></slot>
</div>
具名插槽访问作用域插槽的 props:
<MyComponent><template #header="headerProps">{{ headerProps.text }} {{ headerProps.count }}</template><!-- 结构赋值的方式 --><template #header="{ text, count }">{{ text }} {{ count }}</template>
</MyComponent>
注意插槽上的 name
是一个 Vue 特别保留的 attribute,不会作为 props 传递给插槽。因此最终 headerProps
的结果是 { text: 'hello', count: 1 }
。
如果同时使用了具名插槽与默认插槽,则需要为默认插槽使用显式的 <template>
标签。
直接为组件添加 v-slot
指令将导致编译错误。这是为了避免因默认插槽的 props 的作用域而困惑。
示例:
<!-- <MyComponent> template -->
<div><slot :count="1"></slot><slot name="footer" />
</div>
使用时,没有为默认插槽使用显式的 <template>
标签:
<!-- 该模板无法编译 -->
<MyComponent v-slot="{ count }"><p>{{ count }}</p><template #footer><!-- count 属于默认插槽,此处不可用 --><p>{{ count }}</p></template>
</MyComponent>
为默认插槽使用显式的 <template>
标签有助于更清晰地指出 count
属性在其他插槽中不可用:
<MyComponent><!-- 使用显式的默认插槽 --><template #default="{ count }"><p>{{ count }}</p></template><template #footer><p>这是给底部插槽的内容</p></template>
</MyComponent>