技术栈
- vite
- vue3
- element-plus
从父子组件的传值开始
父子组件传值可以通过 prosp + emit 来实现,虽然 props 可以传递各种类型,但是却不能传递组件(包括HTML),这样灵活度就差了一些。那么怎么办呢?为了提高灵活性,Vue 提供了插槽功能。
插槽可以分为:插槽、具名插槽、作用域插槽如果不明所以的话,可以换一种名称:匿名插槽、命名插槽、可传参插槽。
匿名插槽
如何理解插槽呢?可以先看看div,div是一个容器,里面可以放各种HTML标签,同时也可以放各种组件。
那么我们可以把div内部的标签、组件视为插槽内容,同理,我们也可以把 select 内部的 option 也视为插槽内容。
我们可以用匿名插槽的方式,写一个my-div的组件。
- 子组件 ./comp/my-div.vue
<div style="margin: 10px;padding: 10px; border:1px solid orange;"> 匿名插槽: 插槽前<br><br> <slot>没有设置插槽</slot> <br><br> 插槽后 </div>
子组件设置一个 slot 标签,slot 可以理解为是一种“插值”,表示父组件的插槽在这个位置被渲染,然后在其前后可以加入子组件自己的内容。
slot 里面是“备用内容”,如果父组件没有设置插槽的话,“备用内容”会被渲染,否则会被忽略。
- 父组件
我们看一看在父组件里面的使用情况:
import myDiv from'./comp/my-div.vue'
匿名插槽<br> 设置文本框作为插槽内容: <my-div> <input type="text" placeholder="父组件的插槽"> </my-div> <br> 没有设置插槽内容: <br> <my-div></my-div>
- 看看效果
匿名插槽
这样就实现了一个简单的具有插槽功能的组件,当然这个组件是为了插槽而插槽,并没有没有实际意义。
那么插槽在实际项目里可以有哪些作用呢?我们可以参考一下UI库的组件,他们有很多插槽的实际应用,比如 el-input、el-table等。
具名插槽。
“具名”是个啥意思?感觉用“命名插槽”更好理解一些。
- 如果一个组件只有一个插槽,那么不用写名称,Vue会使用默认名称:default 。
- 如果一个组件有多个插槽的话,那么就需要起名来区分不同的插槽。
el-input 提供了prefix、suffix、prepend、append四个插槽,就是采用了命名插槽的方式。
我们来看一下官网的例子:
<el-input v-model="input1" placeholder="Please input"> <template #prepend>Http://</template> </el-input> <el-input v-model="input2" placeholder="Please input"> <template #append>.com</template> </el-input>
- # 是 v-lot: 的简写形式,类似于 “v-bind:” 简写为 “:”,“v-on:” 简写为 “@”
- prepend 在文本框的前面放置一个插槽,比如 http://
- append 在文本框的后面方式一个插槽,比如 .com
这样可以方便输入URL地址。其实如果 append 放置一个 el-autocomplete 的话,可以更灵活的设置域名后缀。
手写一个命名插槽
还是手写一个命名插槽,看一下子组件的实现方式。
- 子组件 ./comp/my-div-name.vue
<div style="margin: 10px;padding: 10px; border:1px solid rgba(61, 67, 155, 0.692);"> <slot name="header">我来组成头部</slot> 插槽中间内容 <slot name="footer">我来组成结尾</slot> </div>
实现具名插槽的方式很简单,用 name 属性设置插槽的名称即可。
- 父组件的调用
import myDivName from'./comp/my-div-name.vue'
<my-div-name> <template v-slot:header> <h1>这是头部</h1> </template> <template #footer> <p>这是结尾</p> </template> </my-div-name>
父组件需要用 template 限定具名插槽内容的范围,我们来看看效果:
具名插槽
作用域插槽
插槽是父组件的,不是子组件的,父组件可以完全操作插槽里的组件。但是子组件只能规定插槽的渲染位置,其他的就不能操作了,这样的话还是有些不够灵活,于是出现了作用域插槽。
作用域插槽的目的是解决父组件、子组件、插槽之间的数据通讯的问题。
还是看看UI库组件 el-table 的插槽 。
父组件设置列表数据,传递给子组件,子组件渲染 table 表格。为了更灵活,组件提供了自定义列的功能,采用的就是作用域的插槽。
看一下官网示例:
<el-table :data="tableData" style="width: 100%"> <el-table-column label="Date" width="180"> <template #default="scope"> <span style="margin-left: 10px">{{ scope.row.date }}</span> </template> </el-table-column> </table>
- scope 就是子组件传递出来的数据集合,包含row、column、$index等属性。
const tableData = reactive([ { date: '2016-05-03', name: 'Tom', address: 'No. 189, Grove St, Los Angeles', }, { date: '2016-05-02', name: 'Tom', address: 'No. 189, Grove St, Los Angeles', }, { date: '2016-05-04', name: 'Tom', address: 'No. 189, Grove St, Los Angeles', }, { date: '2016-05-01', name: 'Tom', address: 'No. 189, Grove St, Los Angeles', } ])
- tableData:父组件定义数据列表,通过 data 属性传递给子组件。
这里的 scope 的数据流程是这样的:父组件 =》子组件 =》插槽。
为啥要绕一圈呢?虽然父组件可以直接给插槽设置值,但是由于 tr 是循环出来的,父组件无法获知循环到哪一行了,所以需要子组件告知循环行数,这个信息就是通过作用域插槽来实现的,我们可以做一个简单的示例。
手撸一个简单的作用域插槽
- 子组件 ./comp/my-table.vue
<div> <table> <tr> <th>标题一</th> <th>标题二</th> <th>自定义</th> </tr> <tr v-for="(item, index) in data" :key="index" > <td>{{item.t1}}</td> <td>{{item.t2}}</td> <td> <slot name="td" :row="item" :$index="index" ></slot> </td> </tr> </table> </div>
第三列设置一个具名插槽,通过row、$index 传递数据。
const props = defineProps({ data: Array })
设置一个属性,接收列表数据。
- 父组件调用
import myTable from'./comp/my-table.vue' const data = reactive([ { t1: '11', t2: '12', t3: '13' }, { t1: '21', t2: '22', t3: '23' }, { t1: '31', t2: '32', t3: '33' } ])
<my-table :data="data"> <template #td="scope"> 自定义列:{{scope}} </template> </my-table>
可以看到数据的传递。
子组件的插槽,先起个名字,就叫做“td”好了,不要纠结名称,俺有起名困难症。
然后用 row 属性传递行的数据,用 $index 传递遍历到第几行的数据。
这样一个简单的作用域插槽就搞定了。当然只是一个示例,还是没有啥实际意义。
那么有实际意义的是什么样子的呢?还记得标题吗?我可不是标题党,彩蛋马上就来。
片尾彩蛋
现在流行用 json 来渲染组件,还是用 el-table 举例,我们可以定义一个 json,来描述表格列的情况,比如:
{ "itemMeta": [ { "prop": "name", "label": "姓名", "width": 140, "align": "center", "header-align": "center" }, { "prop": "age", "label": "年龄", "width": 140, "align": "center", "header-align": "center" }, { "prop": "mobile", "label": "电话", "width": 140, "align": "center", "header-align": "center" }, { "prop": "url", "label": "URL", "width": 140, "align": "center", "header-align": "center" } ] }
然后遍历 el-table-colmun 设置属性,这样就可以实现动态渲染 table 的功能。这样虽然很方便,但是自定义列呢?如果不支持插槽的话,那么灵活性就差了一些。
鱼和熊掌能不能兼得呢?既然都写到这里了,那么肯定可以兼得。
做一个默认规则
自定义列的插槽名称格式:td_{字段名称}。也就是说 td_开头的视为自定义列的插槽,加上前缀可以避免和 el-table 自带的具名插槽冲突。
然后封装一下 el-table
建立一个组件 ./comp/my-table-json.vue
import { useSlots } from'vue' const props = defineProps({ colInfo: Object, data: Array }) // 获取插槽信息 const slots = useSlots() // 获取列的描述信息 const colInfo = props.colInfo // 检查插槽,设置名称 colInfo.forEach(col => { const _slotName = 'td_' + col.prop if (typeof slots[_slotName] === 'function') { // 有插槽 col.slotName = _slotName } else { // 没有插槽 col.slotName = '' } })
定义属性,接收数据和列的描述。然后获取插槽的信息,设置列是否需要加载插槽。
<el-table :data="data" style="width: 100%"> <template v-for="(item, index) in colInfo" :key="index" > <!--不带插槽的列--> <el-table-column v-if="item.slotName == ''" v-bind="item" > </el-table-column> <!--带插槽的列--> <el-table-column v-else v-bind="item" > <template #default="scope"> <slot :name="item.slotName" v-bind="scope"></slot> </template> </el-table-column> </template> </el-table>
遍历列的描述信息,判断是否需要加载插槽,如果需要插槽的话,设置插槽并且传递 scope 数据。
父组件的调用
父组件就简单多了。
UI库的 table 的二次封装 不用自定义列: <my-table-json :data="data" :colInfo="colInfo"> </my-table-json> 使用自定义列: <my-table-json :data="data" :colInfo="colInfo"> <template #td_url="{ row }"> <a :href="row.url" target="blank">{{row.name}}</a> </template> <template #td_mobile="scope"> 手机:{{scope.row.mobile}} </template> </my-table-json>
不需要自定义列的话,代码可以更简洁;需要自定义列的话,也支持用插槽的方式实现。
import myTableJson from'./comp/my-table-json.vue' import meta from'./grid.json' const colInfo = reactive(meta.itemMeta) const data = reactive([ { name: '阿蒙', age: 18, mobile: '1399999991', url: 'https://naturefw.gitee.io/nf-rollup-ui-controller' }, { name: '小李', age: 18, mobile: '1399999992', url: 'https://naturefw.gitee.io/nf-rollup-ui-controller/meta-base' }, { name: '路飞', age: 18, mobile: '1399999993', url: 'https://naturefw.gitee.io/nf-rollup-ui-controller/meta-base' } ])
这样就不用手撸 el-table-column 了,交给子组件即可,同时还可以满足自定义列的需求。
是不是即简洁又灵活。这个彩蛋还满意吧。
看看效果:
json渲染 + 作用域插槽
在线演示
https://naturefw.gitee.io/nf-rollup-ui-controller/test-slot
源码
https://gitee.com/naturefw/nf-rollup-ui-controller/tree/master/src/views/test/slot
本文作者:自然框架
个人网址:jyk.cnblogs.com
声明:本文为 脚本之家专栏作者 投稿,未经允许请勿转载。