vue.js

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > vue.js > Slots Emit Props穿透组件封装

Slots Emit和Props穿透组件封装实现摸鱼加钟

作者:Gnod

这篇文章主要为大家介绍了Slots Emit和Props穿透组件封装实现示例详解,为摸鱼加个钟,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

👀背景

组内多人共同开发时免不了基于某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?: String
	handleClose?: 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>
			<template
                            v-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-xxxCard组件Slot,b-xxxButton组件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中直接使用了

<template>
	<div class="procard-container">
		<PureCard
			@close="onEmitClose"
			:handleClose="handleClose"
		>
			<!-- 使用组装好的cardSlots -->
			<template
				v-for="(slotKey, slotIndex) in cardSlots"
				:key="slotIndex"
				v-slot:[slotKey]
			>
				<slot :name="`c-${slotKey}`">{{slotKey}}</slot>
			</template>
		</PureCard>
		<PureButton
			@click="onButtonClick"
			:handleClick="handleButtonClick"
		>
			<!-- 使用组装好的buttonSlots -->
			<template
				v-for="(slotKey, slotIndex) in buttonSlots"
				:key="slotIndex"
				v-slot:[slotKey]
			>
				<slot :name="`b-${slotKey}`">{{slotKey}}</slot>
			</template>
		</PureButton>
	</div>
</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 -->
        <template
                v-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>
    <ProCard
      title="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>

看一下实际效果

点击一下右上角关闭按钮

可以看到成功穿透了Emit和Props并且被子组件给执行了

Props和Emit穿透方案-多子组件

多子组件的情况下Props和Emit穿透的解决方案也很多,比如和Slots一样采用前缀的方式来分别组装,但是这种方式较为繁琐,这里比较推荐使用Props分组的方案,在传入的时候就直接把

ProCard

interface ProCardProps {
	title: String
	cardProps: Object // 新增cardProps,用来转发外部传入用于card组件的props
        buttonProps: 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 -->
			<template
				v-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 -->
			<template
				v-for="(slotKey, slotIndex) in buttonSlots"
				:key="slotIndex"
				v-slot:[slotKey]
			>
				<slot :name="`b-${slotKey}`"></slot>
			</template>
		</PureButton>
	</div>
</template>

使用方法如下

<template>
  <div>
  <!-- 这边把之前的@close和:handleClose改写如下,从cardProps传入 -->
    <ProCard
      title="123"
      :cardProps="{
        onClose: 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>
  </div>
</template>

点击Card组件关闭图标再单机Button组件之后效果如下

可以看到传入的cardPropsbuttonProps都起到了预期的效果

最后

希望本文可以让你有所收获,这是我在掘金写的第一篇文章,希望可以帮助到大家。Slots、Emit、Props穿透的方案有很多,本文介绍的是我在项目中实际使用到的几种方法,尤其是在重度依赖第三方UI组件库的的情况下特别适用,既能很好的兼顾三方组件库的原生Api,也能在此基础上进行增量扩展。

最后,XDM!给摸鱼的时间加钟吧!

更多关于Slots Emit Props穿透组件封装的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文