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穿透組件封裝的資料請關註WalkonNet其它相關文章!

推薦閱讀: