Vue演练场基础知识(七)插槽
为学习Vue基础知识,我动手操作通关了Vue演练场,该演练场教程的目标是快速体验使用 Vue 是什么感受,设置偏好时我选的是选项式 + 单文件组件。以下是我结合深入指南写的总结笔记,希望对Vue初学者有所帮助。
文章目录
- 十五. 插槽
- 插槽内容与出口
- 渲染作用域
- 默认内容
- 具名插槽
- 条件插槽
- 动态插槽名
- 作用域插槽
- 具名作用域插槽
- 高级列表组件示例
- 无渲染组件
十五. 插槽
插槽内容与出口
父组件可以用props
向子组件传递js表达式,emits
向子组件传递事件,那能不能直接向子组件传递定义好的模板内容呢?
这就是插槽的作用。
<!-- Dialog.vue -->
<div class="dialog">
<slot />
</div>
<Dialog>
<div>对话框内容</div>
</Dialog>
最终渲染出的 DOM 是这样:
<div class="dialog">
<div>对话框内容</div>
</div>
渲染作用域
由于插槽内容是在父组件里定义的,所以它能访问到父组件的数据作用域,不能访问子组件的。
<!-- Dialog.vue -->
<script>
export default {
data() {
return {msg: '来自子组件的内容'};
}
}
</script>
<template>
<div class="dialog">
<slot />
</div>
</template>
<script>
import Dialog from './Dialog.vue';
export default {
components: {Dialog},
data() {
return {msg: '来自父组件的内容'};
}
}
</script>
<template>
<Dialog>
<div>{{msg}}</div>
</Dialog>
</template>
最终渲染出的 DOM 是这样:
<div class="dialog">
<div>来自父组件的内容</div>
</div>
默认内容
<slot>
标签之间的内容可以作为默认内容,如果父组件使用了含有插槽的子组件但没有传入插槽内容,子组件中的插槽就使用默认内容。
<!-- Dialog.vue -->
<div class="dialog">
<slot>默认内容</slot>
</div>
<Dialog>
<div>对话框内容</div>
</Dialog>
<Dialog />
最终渲染出的 DOM 是这样:
<div class="dialog">
<div>对话框内容</div>
</div>
<div class="dialog">
默认内容
</div>
具名插槽
有时候我们希望子组件能接收多个插槽内容,比如希望一个对话框组件支持分别接收头部内容、主体内容、底部内容。对于这种场景, 元素可以有一个特殊的 attribute name
,用来给各个插槽分配唯一的 ID。
这类带 name 的插槽被称为具名插槽 (named slots)。没有提供 name 的 <slot>
出口会被隐式地命名为“default”。
<!-- Dialog.vue -->
<div class="dialog">
<!-- 对话框头部 -->
<header class="card-header">
<slot name="header" />
</header>
<!-- 对话框主体 -->
<main class="card-main">
<slot></slot>
</main>
<!-- 对话框尾部 -->
<footer class="card-footer">
<slot name="footer">默认底部</slot>
</footer>
</div>
与之<slot name="header">
匹配的是包含模板内容的<template v-slot:header>
,或简写为<template #header>
:
<Dialog>
<!-- 对话框头部 -->
<template #header>
<div>我的标题</div>
</template>
<!-- 对话框主体 -->
<template #default>
<p>我的内容1</p>
<p>我的内容2</p>
</template>
<!-- 对话框尾部 -->
<template #footer>
<button>取消</button>
<button>确定</button>
</template>
</Dialog>
当一个组件同时接收默认插槽和具名插槽时,所有位于顶级的非 <template>
节点都被隐式地视为默认插槽的内容。所以也可以省略掉<template #default>
,把其中内容直接作子组件的直接子元素,但不能混用,即子组件的直接子元素中不能同时出现<template #default>
和非 <template>
节点。
<Dialog>
<!-- 对话框头部 -->
<template #header>
<div>我的标题</div>
</template>
<!-- 对话框主体 -->
<p>我的内容1</p>
<p>我的内容2</p>
<!-- 对话框尾部 -->
<template #footer>
<button>取消</button>
<button>确定</button>
</template>
</Dialog>
最终渲染出的 HTML 如下:
<div class="dialog">
<!-- 对话框头部 -->
<header class="card-header">
<div>我的标题</div>
</header>
<!-- 对话框主体 -->
<main class="card-main">
<p>我的内容1</p>
<p>我的内容2</p>
</main>
<!-- 对话框尾部 -->
<footer class="card-footer">
<button>取消</button>
<button>确定</button>
</footer>
</div>
推荐在使用具名插槽的时候,为默认插槽使用显式的 <template>
标签,不容易混淆,更加可读。
条件插槽
在上面的例子中,我们为对话框组件的 header、footer 或 default 等插槽设置了margin等样式:
<header class="card-header">
<slot name="header" />
</header>
但如果用户希望创建一个没有头部的对话框,于是不传header插槽内容(且没有默认插槽内容),那就会出现对话框顶部渲染出一个空的<header>
。
<header class="card-header"></header>
那么有没有办法在根据header插槽存在与否来决定要不要渲染<header>
标签呢?
可以结合 $slots 属性与 v-if 来实现:
<header class="card-header" v-if="$slot.header">
<slot name="header" />
</header>
其中的$slots
表示父组件传入插槽的对象。
通常用于手写渲染函数,但也可用于检测是否存在插槽。
每一个插槽都在 this.$slots
上暴露为一个函数,返回一个 vnode 数组,同时 key 名对应着插槽名。默认插槽暴露为 this.$slots.default
。
// this.$slots等于
{
default: () => <div>...</div>,
header: () => <div>...</div>,
footer: () => <div>...</div>,
}
动态插槽名
插槽名不仅可以设置为常量,还可以设置为变量,如以下设置插槽名为变量mySlotName
:
<!-- Dialog.vue -->
<div class="dialog">
<slot :name="mySlotName">默认底部</slot>
</div>
<Dialog>
<template #[mySlotName]>
<button>取消</button>
<button>确定</button>
</template>
</Dialog>
作用域插槽
上文中提到,插槽是在父组件中被定义的,所以无法读取到子组件的状态。那假如插槽需要拿到子组件状态该怎么办呢?
可以像对组件传递 props 那样,向插槽出口<slot>
上传入 attributes,实现把子组件的变量传递到插槽内容:
<!-- Dialog.vue -->
<div class="dialog">
<slot msg="来自子组件的内容" /> <!-- 将子组件的状态传入slot -->
</div>
<Dialog v-slot="slotProps"> <!-- 设置一个slotProps变量接收来自父组件的插槽Props -->
<div>{{slotProps.msg}}</div>
</Dialog>
// 或
<Dialog v-slot="{msg}">
<div>{{msg}}</div>
</Dialog>
最终渲染出的 DOM 是这样:
<div class="dialog">
<div>来自子组件的内容</div>
</div>
具名作用域插槽
作用域插槽也可以与具名插槽混用,如下面的v-slot:header="slotProps1"
(也可写作#header="slotProps1
):
<!-- Dialog.vue -->
<div class="dialog">
<header>
<slot name="header" msg="来自子组件的内容1" />
</header>
<main>
<slot msg="来自子组件的内容2"/>
</main>
</div>
<Dialog>
<!-- header插槽 -->
<!-- v-slot设置在<template>上,而不是<Dialog>上 -->
<!-- v-slot:header="slotProps1" 也可以简写成 #header="slotProps1" -->
<template #header="slotProps1">
<div>来自父组件的内容</div>
<div>{{slotProps1.msg}}</div>
</template>
<!-- 默认插槽 -->
<!-- 支持在 v-slot 中使用解构 -->
<!-- 可写作 v-slot="{msg}" 或 v-slot:default="{msg}" 或 #default="{msg}" -->
<template #default="{msg}">
<div>{{msg}}</div>
</template>
</Dialog>
最终渲染出的 DOM 是这样:
<div class="dialog">
<header>
<div>来自父组件的内容</div>
<div>来自子组件的内容1</div>
</header>
<main>
<div>来自子组件的内容2</div>
</main>
</div>
再次推荐为默认插槽使用显式的 <template>
标签,不容易出错。
不允许像下面这样
同时在子组件上和template上定义v-slot,否则会编译报错。
<Dialog v-slot="slotProps2">
<!-- header插槽 -->
<template v-slot:header="slotProps1">
<div>来自父组件的内容</div>
<div>{{slotProps1.msg}}</div>
</template>
<!-- 默认插槽 -->
<div>{{slotProps2.msg}}</div>
</Dialog>
高级列表组件示例
通过对具名作用域插槽的运用,我们可以实现一个高级列表组件,封装了可重用的逻辑 (如数据获取、分页、无限滚动等) 和视图输出,并将部分视图输出(如列表项的内容和样式、每页条目数)通过作用域插槽交给了消费者组件来管理。
<!-- FuncyList.vue -->
<ul>
<li v-for="item in list" key="item.id">
<slot name="itemSlot" v-bind="item">
{{item}}
</slot>
</li>
每页{{pageNum}}个
</ul>
<FuncyList :page-num="10" >
<template #itemSlot="{id, title}">
<div>{{id}}-{{title}}</div>
</template>
</FuncyList>
无渲染组件
一些组件可能只包括了逻辑而不需要自己渲染内容,视图输出通过作用域插槽全权交给了消费者组件。我们将这种类型的组件称为无渲染组件。
<!-- NoRenderComponent.vue -->
<script>
export default {
data() {
return {pi: '3.1415926535'};
},
computed: {
doublePi: () => this.pi *2;
}
}
</script>
<template>
<!-- 仅包含逻辑,不包含任何视图 -->
<slot :text="doublePi" />
</temlate>
<NoRenderComponent >
<template v-slot="{text}">
<!-- 展示NoRenderComponent数据的视图写在插槽里 -->
<div class="uiClass">{{text}}</div>
</template>
</NoRenderComponent>