目录
背景
准备工作
Slots穿透方案-单子组件
Slots穿透方案-多子组件
Props和Emit穿透方案-单子组件
Props和Emit穿透方案-多子组件
最后
背景
组内多人共同开发时免不了基于某UI库二次封装组件来适应项目业务场景的情况,但不知道大家有没有遇到过需要兼容部分或者穿透子组件全部
Props
或者Slots
的情况,这种时候如果针对每一个Props或者Slots去单独处理穿透不仅费时费力而且代码会越来越臃肿难以维护,所以想在这里通过一个简单的例子来对比一下Slots、Props、Emit的各种穿透方案
准备工作
首先新建我们需要用到的子组件,如下
Card.vue
<template><div class="card-container"><div @click="handleClose" class="card-close"><!-- 先用X来代替 --><span>X</span></div><div class="card-title"><slot name="title"><!-- 默认使用props作为title,有slot则优先slot -->{{props.title}}</slot></div><div class="card-content"><slot><!-- content这里也是,一切都以slot优先 -->{{props.content}}</slot></div><div class="card-footer"><slot name="footer"><!-- footer这里也是,一切都以slot优先 -->{{props.footer}}</slot></div></div>
</template><script lang="ts" setup>
import { defineProps, defineEmits } from 'vue'interface ChildrenProps {title?: StringhandleClose?: Function
}const props = defineProps<ChildrenProps>()const emits = defineEmits(['close'])// 响应点击事件
const handleClose = () => {// 这边演示方便,直接调props之后跟上emit调用props.handleClose && props.handleClose()emits('close')}
</script>
...css部分略过
再来准备一个Button.vue
<template><button @click="handleClick"><slot name="prefix"></slot><slot>{{props.title}}</slot><slot name="suffix"></slot></button>
</template><script lang="ts" setup>
import { withDefaults, defineProps, defineEmits } from 'vue'
interface ButtonProps {title?: string,handleClick?: Function
}const emits = defineEmits(['click'])const props = withDefaults(defineProps<ButtonProps>(), {title: 'DONE'
})const handleClick = () => {emits('click')props.handleClick && props.handleClick()
}</script>
以及我们需要实现的ProCard.vue
Slots穿透方案-单子组件
使用Vue
提供的Dynamic directive arguments
结合v-slot
指令
Dynamic directive arguments部分文档链接
在单子组件的情况下穿透Slot比较简单,不需要考虑太多的Slot覆盖问题,只需要关注封装组件自身Slot命名即可,如有命名重复情况可参考多子组件方案解决,比如下面这个ProCard.vue
,只用到了Card
组件。
<template><div class="procard-container"><PureCard><templatev-for="(slotKey, slotIndex) in slots":key="slotIndex" v-slot:[slotKey]><slot :name="slotKey"></slot></template></PureCard></div>
</template><script lang="ts" setup>
import { useSlots } from 'vue'
import PureCard from '../Card/Card.vue'const slots = Object.keys(useSlots())
</script>
使用
<template><div><ProCard><template #title><span>CardSlot标题</span></template></ProCard></div>
</template>
Slots穿透方案-多子组件
通常我们封装业务组件时一般不至于一个子组件,但多个子组件的情况下就要特别注意Slot命名情况了,这边分享一个在平时开发时我们选择的一个方案:使用不同前缀来区分不同slot,props也是同理。在ProCard.vue
中我们加入一个Button
组件,协商约定c-xxx
为Card
组件Slot,b-xxx
为Button
组件Slot,这样在经过分解之后就可以区分出应该往哪个组件穿透Slot了。
在ProCard.vue
中取的所有slots并且理好各个组件所需slots
// 首先还是取到所有Slots的key
const slots = Object.keys(useSlots())// 定义一个buttonSlots,用来组装需要用到的Button组件的slots
const buttonSlots = ref<string[]>([])// 定义一个cardSlots,用来组装需要用到的Card组件的slots
const cardSlots = ref<string[]>([])// 找出各自组件需要的slot组装好push进去就可以在template中穿透到指定组件了
for (let slotIndex = 0; slotIndex < slots.length; slotIndex++) {const slotKey = slots[slotIndex];if (slotKey.indexOf('c-') > -1) {cardSlots.value.push(slotKey.slice(2, slotKey.length))continue}if (slotKey.indexOf('b-') > -1) {buttonSlots.value.push(slotKey.slice(2, slotKey.length))}
}
接下来就可以在template中直接使用了
引入一下ProCard
组件来看一下效果吧
<template><div><ProCard title="123"><template #c-title><span>CardSlot标题</span></template><template #c-default><span>CardSlot内容</span></template><template #c-footer><span>CardSlot底部</span></template><template #b-default>按钮</template></ProCard></div>
</template>
成功实现了多组件Slots穿透
Props和Emit穿透方案-单子组件
Props和Emit的穿透方式与Slots的方案类似,使用v-bind
直接绑定组件Attributes
是最方便的穿透方式,但缺点也很明细,直接v-bind
所有Attributes
可能会导致命名重复所带来的各种连锁问题,如果像上文slots
一样通过前缀来区分组装又有点繁琐,所以如果是多子组件的情况下推荐使用下面的props+v-bind
方案。
单子组件这边在ProCard
中使用useAttrs
来得到子组件上所有的attributes,再使用v-bind
直接转发到ProCard
的子组件,这样就可以直接穿透Props和Emit了非常方便好用
// 获取到组件所有的attributes
const attrs = useAttrs()
template中转发到子组件
<PureCard@close="onEmitClose":handleClose="handleClose"v-bind="attrs"
><!-- 使用组装好的cardSlots --><templatev-for="(slotKey, slotIndex) in cardSlots":key="slotIndex"v-slot:[slotKey]><slot :name="`c-${slotKey}`"></slot></template>
</PureCard>
父组件调用ProCard
<script setup lang="ts">
import ProCard from './components/ProCard/ProCard.vue'const handleClose = () => {console.log('parent handleClose')
}
const onClose = () => {console.log('parent onClose')
}
</script>
<template><ProCardtitle="123"@close="onClose":handleClose="handleClose"><template #c-title><span>CardSlot标题</span></template><template #c-default><span>CardSlot内容</span></template><template #c-footer><span>CardSlot底部</span></template><template #b-default>按钮</template></ProCard>
</template>
Props和Emit穿透方案-多子组件
多子组件的情况下Props和Emit穿透的解决方案也很多,比如和Slots一样采用前缀的方式来分别组装,但是这种方式较为繁琐,这里比较推荐使用Props分组的方案,在传入的时候就直接把
ProCard
interface ProCardProps {title: StringcardProps: Object // 新增cardProps,用来转发外部传入用于card组件的propsbuttonProps: Object // 新增buttonProps,用来转发外部传入用于button组件的props
}// 获取到组件所有的attributes
const attrs = useAttrs()const props = defineProps<ProCardProps>()// 在template中使用如下,注意替换Card组件和Button组件的v-bind为各自需要接收的props<template><div class="procard-container"><PureCard@close="onEmitClose":handleClose="handleClose"v-bind="props.cardProps"><!-- 使用组装好的cardSlots --><templatev-for="(slotKey, slotIndex) in cardSlots":key="slotIndex"v-slot:[slotKey]><slot :name="`c-${slotKey}`"></slot></template></PureCard><PureButton@click="onButtonClick":handleClick="handleButtonClick"v-bind="props.buttonProps"><!-- 使用组装好的buttonSlots --><templatev-for="(slotKey, slotIndex) in buttonSlots":key="slotIndex"v-slot:[slotKey]><slot :name="`b-${slotKey}`"></slot></template></PureButton></div>
</template>
最后
希望本文可以让你有所收获,Slots、Emit、Props
穿透的方案有很多,本文介绍的是我在项目中实际使用到的几种方法,尤其是在重度依赖第三方UI组件库的的情况下特别适用,既能很好的兼顾三方组件库的原生Api,也能在此基础上进行增量扩展。