认识插槽Slot
在开发中,我们会经常封装一个个可复用的组件:
- 我们会通过
props
传递给组件一些数据
,让组件来进行展示; - 但是为了让这个组件具备更强的通用性,我们不能将
组件中的内容
限制为固定的div
、span
等等这些元素;- 比如某种情况下我们使用组件,希望组件显示的是一个按钮,某种情况下我们使用组件希望显示的是一张图片;
- 我们应该让使用者可以决定某一块区域到底存放什么内容和元素;
举个栗子:假如我们定制一个通用的导航组件 - NavBar
- 这个组件分成三块区域:左边-中间-右边,每块区域的内容是不固定;
- 左边区域可能显示一个菜单图标,也可能显示一个返回按钮,可能什么都不显示;
- 中间区域可能显示一个搜索框,也可能是一个列表,也可能是一个标题,等等;
-
右边可能是一个文字,也可能是一个图标,也可能什么都不显示;
image.png
这个时候我们就可以来定义插槽slot:
- 插槽的使用过程其实是
抽取共性、预留不同
; - 我们会将
共同的元素
、内容
依然在组件内进行封装; - 同时会将
不同的元素
使用slot
作为占位,让外部决定到底显示什么样的元素;
如何使用slot呢?
- Vue中将
<slot>
元素作为承载分发内容的出口; - 在封装组件中,使用特殊的元素
<slot>
就可以为封装组件开启一个插槽
; - 该插槽插入
什么内容
取决于父组件
如何使用;
插槽的基本使用
插槽分为匿名插槽(默认插槽)
,具名插槽
, 作用域插槽
匿名插槽的使用
插槽的基本使用
1. 在组件SlotCpn.vue
中预留匿名插槽
<template>
<div>
<div>组件开始</div>
<slot></slot>
<div>组件结束</div>
</div>
</template>
2. 在组件App.vue
中使用组件SlotCpn.vue
,并在组件开始标签<slot-cpn>
和结束标签</slot-cpn>
插入的所有内容,都会被插入到组件的所有的匿名插槽
一份
<template>
<div>
<!-- 不插入任何内容 -->
<slot-cpn></slot-cpn>
<slot-cpn>大风预警</slot-cpn>
<slot-cpn>
<button>添加</button>
</slot-cpn>
<!-- 使用组件时,不管在组件开始标签和结束标签传几项,都会全部传给默认插槽 -->
<slot-cpn>
<input type="text" />
<button>提交</button>
</slot-cpn>
<slot-cpn>
<i>why</i>
<span>水果</span>
<button>按钮</button>
</slot-cpn>
</div>
</template>
<script>
import SlotCpn from "./SlotCpn.vue";
export default {
components: {
SlotCpn,
},
};
</script>
执行npm run serve,并在浏览器预览,可以看到,使用组件时在组件开始标签
和结束标签
插入的所有内容
都被插入到匿名插槽
所在位置了
3. 如果在组件SlotCpn.vue
中预留了多个匿名插槽
<template>
<div>
<div>组件开始</div>
<slot></slot>
<div>---分割线---</div>
<slot></slot>
<div>组件结束</div>
</div>
</template>
执行npm run serve,并在浏览器预览,可以看到,在组件SlotCpn.vue
中的每个匿名插槽的位置都被插入了使用组件时在组件开始标签
和结束标签
插入的所有内容
4. 如果希望在使用组件SlotCpn.vue
时,在组件开始标签
和结束标签
中不插入内容时,显示默认内容
,我们可以给插槽设置默认值
<template>
<div>
<div>组件开始</div>
<slot>
<h1>我是插槽1的默认值</h1>
</slot>
<div>---分割线---</div>
<slot>
<h1>我是插槽2的默认值</h1>
<button>提交</button>
</slot>
<div>组件结束</div>
</div>
</template>
执行npm run serve,并在浏览器预览,可以看到,使用组件时在组件开始标签
和结束标签
不插入内容,在匿名插槽的位置会显示设置的默认值;
使用组件时在组件开始标签
和结束标签
插入了内容,在组件SlotCpn.vue
中的每个匿名插槽的位置都被插入了使用组件时在组件开始标签
和结束标签
插入的所有内容
具名插槽的使用
如果希望把插入在组件开始标签
和结束标签
中的内容的不同部分
插入到组件的指定
插槽位置,可以使用具名插槽
-
具名插槽
顾名思义就是给插槽起一个名字,<slot>
元素有一个特殊的 attribute:name
,可以通过给name赋值,给插槽起名 - 一个不带 name 的slot,会带有隐含的名字
default
,即name='default'
,匿名插槽其实就是名字为default的插槽 - 在使用组件时,可以在组件
开始标签
和结束标签
中间使用template
标签,给template标签通过绑定v-slot:插槽名
来指定template标签之间的内容插入到哪个具名插槽的位置
组件NavBar.vue
中预留了三个具名插槽,名字分别为'left', 'right', 'center'
<template>
<div class="tabs">
<div class="left">
<slot name="left">标题</slot>
</div>
<div class="center">
<slot name="center">
<input />
</slot>
</div>
<div class="right">
<slot name="right">
<span>...</span>
</slot>
</div>
</div>
</template>
<script>
export default {
}
</script>
<style scoped>
.tabs {
display: flex;
align-items: center;
}
.center {
flex: 1;
}
</style>
组件App.vue
<template>
<div>
<nav-bar></nav-bar>
<!-- 具名插槽的v-slot:插槽名字 只能放在template上 -->
<nav-bar>
<!-- 指定其中的内容插入到名字为left的插槽所在的位置 -->
<template v-slot:left>
<span >中誉</span>
</template>
<!-- 指定其中的内容插入到名字为right的插槽所在的位置 -->
<template v-slot:right>
<button>添加</button>
</template>
<!-- 指定其中的内容插入到名字为center的插槽所在的位置 -->
<template #center>
<div >详情1</div>
</template>
</nav-bar>
</div>
</template>
<script>
import NavBar from './NavBar.vue'
export default {
components: {
NavBar
}
}
</script>
执行npm run serve,并在浏览器预览,可以看到,插入在组件开始标签
和结束标签
中的内容的不同部分
被插入到组件的指定
插槽位置啦
具名插槽使用的时候缩写
把v-slot:
替换为字符 #
组件App.vue
<template>
<div>
<nav-bar>
<template #left>
<span >中誉</span>
</template>
<template #right>
<button>添加</button>
</template>
<template #center>
<div >详情1</div>
</template>
</nav-bar>
</div>
</template>
<script>
import NavBar from './NavBar.vue'
export default {
components: {
NavBar
}
}
</script>
动态插槽的使用
组件NavBar.vue
中一个具名插槽的名字,是由组件被使用时,动态绑定属性name
的值决定的
<template>
<div class="tabs">
<div class="left">
<slot name="left">标题</slot>
</div>
<div class="center">
<slot name="center">
<input />
</slot>
</div>
<div class="right">
<!-- 动态插槽名 -->
<slot :name="name">
<span>...</span>
</slot>
</div>
</div>
</template>
<script>
export default {
props: {
name: {
type: String,
default: "",
},
},
};
</script>
<style scoped>
.tabs {
display: flex;
align-items: center;
}
.center {
flex: 1;
}
</style>
组件App.vue
<template>
<div>
<!-- 使用组件时给组件的属性name传值 -->
<nav-bar :name="name">
<template v-slot:left>
<span >中誉</span>
</template>
<!-- 动态插槽名 -->
<template v-slot:[name]>
<button>添加</button>
</template>
<template #center>
<div >详情1</div>
</template>
</nav-bar>
</div>
</template>
<script>
import NavBar from './NavBar.vue'
export default {
components: {
NavBar
},
data() {
return {
name: 'right'
}
}
}
</script>
作用域插槽的使用
渲染作用域
在Vue中有渲染作用域的概念:
- 父级模板里的所有内容都是在父级作用域中编译的;
- 子模板里的所有内容都是在子作用域中编译的;
如下例子:
- 子组件
ChildCpn.vue
中的变量title
,可以在子组件的模板中使用 - 在父组件
App.vue
中导入了子组件ChildCpn.vue
,注册为ChildCpn
,并在使用子组件<child-cpn>
时给子组件的默认插槽传入<span>{{title}}</span>
- 虽然
<span>{{title}}</span>
被写在子组件开始标签<child-cpn>和结束标签</child-cpn>之间,但依然是在App.vue的模板中,那它的作用域依然是App.vue的作用域,App.vue的data中没有title变量,所以会报错
image.png
认识作用域插槽
但是有时候我们希望在使用子组件时,在子组件的开始标签
和结束标签
之间为子组件的插槽
插入内容时,可以获取到子组件中的变量
,Vue给我们提供了作用域插槽
;
下面看一个案例:
- 1.在App.vue中定义好数组
names
,通过props
传递给子组件MyCpns.vue组件中 - 2.MyCpns.vue组件中遍历
names
数组,设置匿名插槽
,并把遍历的数组的每一项的索引
和值
,作为插槽属性的值
,绑定在插槽
上, - 这些插槽上绑定的属性名和属性值,除了
name
属性名和值外都被存储在一个对象中,以被获取 - 3.通过
v-slot:插槽名="slotProps"
的方式获取到存储着绑定在对应插槽上的属性名和属性值(name属性名和值除外)的对象slotProps
- 4.通过对对象slotProps成员访问的方式,获取到对应的值,以在父组件App.vue中使用
MyCpns.vue
组件
<template>
<div>
<!--遍历数组names-->
<div v-for="(item, index) in names" :key="item">
<!-- 把数组中当前遍历的元素和索引以插槽属性值的方式绑定到插槽上 -->
<slot :item="item" :index="index"></slot>
</div>
</div>
</template>
<script>
export default {
props: {
names: {
type: Array,
default: () => [],
},
},
};
</script>
App.vue
组件
<template>
<div>
<my-cpn :names="names">
<template v-slot:default="slotProps">
<span>{{ slotProps.index }} - {{ slotProps.item }}</span>
</template>
</my-cpn>
</div>
</template>
<script>
import MyCpn from "./MyCpn.vue";
export default {
components: {
MyCpn,
},
data() {
return {
names: ["john", "kobe", "why"],
};
},
};
</script>
<style scoped></style>
如果我们的插槽是默认插槽default,那么在使用的时候 v-slot:default="slotProps"
可以简写为v-slot="slotProps"
App.vue
组件
<template>
<div>
<my-cpn :names="names">
<template v-slot="slotProps">
<span>{{ slotProps.index }} - {{ slotProps.item }}</span>
</template>
</my-cpn>
</div>
</template>
<script>
import MyCpn from "./MyCpn.vue";
export default {
components: {
MyCpn,
},
data() {
return {
names: ["john", "kobe", "why"],
};
},
};
</script>
独占默认插槽
如果我们在使用组件时,只给组件的默认插槽
插入内容,不管组件在定义时是否设置了具名插槽,组件的标签可以被当做插槽的模板来使用,这样,我们就可以将 v-slot 直接用在组件标签上,这种写法叫做:独占默认插槽
App.vue
组件
<template>
<div>
<!-- 省略了template标签 组件的开始标签和结束标签中的内容会被插入到组件的默认插槽位置 -->
<my-cpn :names="names" v-slot="slotProps">
<span>{{ slotProps.index }} - {{ slotProps.item }}</span>
</my-cpn>
</div>
</template>
<script>
import MyCpn from "./MyCpn.vue";
export default {
components: {
MyCpn,
},
data() {
return {
names: ["john", "kobe", "why"],
};
},
};
</script>
同事给匿名插槽和具名插槽都插入内容
如果我们在使用组件时,给组件的默认插槽和具名插槽都插入内容,则不可省略template标签
App.vue
组件
<template>
<div>
<my-cpn :names="names">
<template v-slot="slotProps">
<span>{{ slotProps.index }} - {{ slotProps.item }}</span>
</template>
<template v-slot:bottom="slotProps">
<span>今天天气不错</span>
</template>
</my-cpn>
</div>
</template>
<script>
import MyCpn from "./MyCpn.vue";
export default {
components: {
MyCpn,
},
data() {
return {
names: ["john", "kobe", "why"],
};
},
};
</script>
MyCpns.vue
组件
<template>
<div>
<div v-for="(item, index) in names" :key="item">
<!--匿名插槽-->
<slot :item="item" :index="index"></slot>
<!--具名插槽-->
<slot name="why">coderwhy</slot>
</div>
</div>
</template>
<script>
export default {
props: {
names: {
type: Array,
default: () => [],
},
},
};
</script>
跨组件插槽的使用
要实现这个目标,需要做:
1.给hy-table.vue设置若干个具名插槽
2.page-content使用子组件hy-table.vue时,给对应具名插槽所在位置插入内容时,插入的内容为设置同名具名插槽,供使用page-content.vue的父组件使用
3.goods.vue使用子组件page-content.vue,给对应插槽位置插入想要插入的内容就行了,插入的内容最终会被传入hy-table.vue中设置的同名具名插槽所在位置
hy-table.vue
我们使用element-plus封装了一个hy-table.vue组件,table中展示哪些列由propList决定,在table.vue中动态设置具名插槽
<template>
<div class="hy-table">
<el-table
:data="listData"
border
style="width: 100%"
>
<template v-for="propItem in propList" :key="propItem.prop">
<el-table-column v-bind="propItem" align="center" >
<template #default="scope">
<!-- 在默认插槽内容,动态注册具名插槽 -->
<slot :name="propItem.slotName" :row="scope.row">{{scope.row[propItem.prop]}}</slot>
</template>
</el-table-column>
</template>
</el-table>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
name: "hy-table",
props: {
listData: {
type: Array,
required: true,
},
propList: {
type: Array,
required: true,
},
},
setup(props, { emit }) {
return { };
},
});
</script>
<style scoped lang="less">
.hy-table {
}
</style>
page-content.vue
在此组件中使用子组件hy-table.vue,给hy-table.vue一部分具名插槽插入固定形式的内容
,另一部分具名插槽内部重新注册
同名具名插槽,供使用page-content.vue的父组件自动义插入内容
<template>
<div class="page-content">
<hy-table
:list-data="list"
:prop-list="propList"
>
<!-- table 列公共插槽:status/createAt/updateAt/operate -->
<template #status="scope">
<el-button
size="mini"
:type="scope.row.enable ? 'primary' : 'danger'"
>{{ scope.row.enable ? "启用" : "禁用" }}</el-button
>
</template>
<template #createAt="scope">
<span>{{ $filter.formatUtcTime(scope.row.createAt) }}</span>
</template>
<template #updateAt="scope">
<span>{{ $filter.formatUtcTime(scope.row.updateAt) }}</span>
</template>
<template #operate">
<div class="operate">
<el-button
type="text"
icon="el-icon-edit"
>编辑</el-button
>
<el-button
type="text"
icon="el-icon-delete"
>删除</el-button
>
</div>
</template>
<!-- 列非公共插槽:非公共插槽中注册具名插槽,供页面使用 -->
<template
v-for="item in otherPropSlots"
:key="item.prop"
#[item.slotName]="scope"
>
<template v-if="item.slotName">
<slot :name="item.slotName" :row="scope.row"></slot>
</template>
</template>
</hy-table>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, ref, watch } from "vue";
import HyTable from "@/base-ui/table";
import { useStore } from "@/store";
export default defineComponent({
name: "page-content",
components: {
HyTable,
},
props: {
propList: {
type: Array,
required: true,
},
pageName: {
type: String,
required: true,
},
},
setup(props, { emit }) {
//1.获取列数据
const store = useStore();
const pageInfo = ref({
pageSize: 10,
pageNum: 1,
});
const getPageData = (queryInfo: any = {}) => {
store.dispatch("system/getPageListAction", {
pageName: props.pageName,
queryInfo: {
offset: pageInfo.value.pageSize * (pageInfo.value.pageNum - 1),
size: pageInfo.value.pageSize,
...queryInfo,
},
});
};
getPageData();
const list = computed(() =>
store.getters["system/pageListData"](props.pageName),
);
//2.从内容配置的属性列表中过滤出有非公共插槽的属性配置数组
const publicPropSlots = ["status", "createAt", "updateAt", "operate"];
const otherPropSlots = props.propList.filter((prop: any) => {
return prop.slotName && !publicPropSlots.includes(prop.slotName);
});
return {
list,
otherPropSlots,
};
},
});
</script>
<style scoped lang="less">
.page-content {
padding: 20px;
border-top: 20px solid #f5f5f5;
}
</style>
goods.vue
在页面中使用page-content组件,并给具名插槽插入内容,最终内容会被插入在hy-table.vue中同名具名插槽所在的位置
<template>
<div class="goods">
<page-content
:prop-list="propList"
pageName="goods"
>
<template #img="scope">
<el-image
style="width: 60px; height: 60px"
:src="scope.row.imgUrl"
:preview-src-list="[scope.row.imgUrl]"
>
</el-image>
</template>
</page-content>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import PageContent from "@/components/page-content";
export default defineComponent({
name: "goods",
components: {
PageContent,
},
setup() {
const propList = [
{ prop: "name", label: "商品名", minWidth: "180" },
{ prop: "oldPrice", label: "原价", minWidth: "180" },
{ prop: "newPrice", label: "现价", minWidth: "180" },
{ prop: "imgUrl", label: "图片", minWidth: "180", slotName: "img" },
{ prop: "status", label: "状态", minWidth: "180", slotName: "status" },
{
prop: "createAt",
label: "创建时间",
minWidth: "180",
slotName: "createAt",
},
{
prop: "updateAt",
label: "更新时间",
minWidth: "180",
slotName: "updateAt",
},
{
label: "操作",
minWidth: "180",
slotName: "operate",
},
],
return {
propList,
};
},
});
</script>
<style scoped lang="less"></style>
此文档主要内容来源于王红元老师的vue3+ts视频教程