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-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中直接使用瞭
<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
組件之後效果如下
可以看到傳入的cardProps
和buttonProps
都起到瞭預期的效果
最後
希望本文可以讓你有所收獲,這是我在掘金寫的第一篇文章,希望可以幫助到大傢。Slots、Emit、Props
穿透的方案有很多,本文介紹的是我在項目中實際使用到的幾種方法,尤其是在重度依賴第三方UI組件庫的的情況下特別適用,既能很好的兼顧三方組件庫的原生Api,也能在此基礎上進行增量擴展。
最後,XDM!給摸魚的時間加鐘吧!
更多關於Slots Emit Props穿透組件封裝的資料請關註WalkonNet其它相關文章!
推薦閱讀:
- Vue3編程流暢技巧使用setup語法糖拒絕寫return
- Vue3常用的通訊方式總結與實例代碼
- Vue3內置組件Teleport使用方法詳解
- vue中正確使用jsx語法的姿勢分享
- vue3中$attrs的變化與inheritAttrs的使用詳解