想到頭禿也想不到的Vue3復(fù)用組件還可以這么hack的用法
前言
無論是 vue 還是 react,當(dāng)遇到多處重復(fù)代碼的時候,我們都會想著如何復(fù)用這些代碼,而不是一個文件里充斥著一堆冗余代碼。
實際上,vue 和 react 都可以通過抽組件的方式來達到復(fù)用,但如果遇到一些很小的代碼片段,你又不想抽到另外一個文件的情況下,相比而言,react 可以在相同文件里面聲明對應(yīng)的小組件,或者通過 render function 來實現(xiàn),如:
const Demo: FC<{ msg: string}> = ({ msg }) => {
return <div>demo msg is { msg } </div>
}
const App: FC = () => {
return (
<>
<Demo msg="msg1"/>
<Demo msg="msg2"/>
</>
)
}
/** render function的形式 */
const App: FC = () => {
const renderDemo = (msg : string) => {
return <div>demo msg is { msg } </div>
}
return (
<>
{renderDemo('msg1')}
{renderDemo('msg2')}
</>
)
}
但對于 .vue 模板 來說,沒法像 react 一樣在單個文件里面聲明其他組件,如果你要復(fù)用代碼,那就只能通過抽離組件的方式。
可是啊可是啊!就如上面 Demo 組件,就零零散散兩三行代碼,抽成一個組件你又覺得沒太必要,那唯一的解決方案就是 CV 大法?(當(dāng)然,如果是諸如 list 之類的,可以用 v-for 代碼,但這里介紹的不是這種場景)
我知道你很急,但你先別急。如果我們可以通過在組件范圍內(nèi)將要復(fù)用的模板圈起來,跟 vue 說,喂,這代碼是我圈起來的,因為我有好幾處要用到,雖然目前你看起來好像不支持這功能,不過,沒事,你實現(xiàn)不了的,我來實現(xiàn)
那大致實現(xiàn)方案就是這樣子啦:
<template>
<DefineFoo v-slot="{ msg }">
<div>Foo: {{ msg }}</div>
</DefineFoo>
...
<ReuseFoo msg="msg1" />
<div>xxx</div>
<ReuseFoo msg="msg2" />
<div>yyy</div>
<ReuseFoo msg="msg3" />
</template>

可是,這種方案究竟如何實現(xiàn)呢?畢竟牛都已經(jīng)吹上天了,實現(xiàn)不了也要硬著頭皮折騰。好了,不賣關(guān)子,antfu 大佬實際上已經(jīng)實現(xiàn)了,叫做createReusableTemplate,且放到 VueUse 里面了,具體可以點擊文檔看看。
接下來我會給大家介紹下 createreusabletemplate 如何使用,以及是如何實現(xiàn)的(不用緊張,上手成本為 0,且看懂只需要 1 分鐘不到)
用法
通過 createReusableTemplate 拿到定義模板和復(fù)用模板的組件
<script setup>
import { createReusableTemplate } from '@vueuse/core'
const [DefineTemplate, ReuseTemplate] = createReusableTemplate()
</script>
然后在你要復(fù)用代碼的地方,將其用DefineTemplate包起來,之后就可以通過ReuseTemplate在單文件 template 的任意地方使用了:
<template>
<DefineTemplate>
<!-- 這里定義你要復(fù)用的代碼 -->
</DefineTemplate>
<!-- 在你要復(fù)用的地方使用ReuseTemplate, -->
<ReuseTemplate />
...
<ReuseTemplate />
</template>
?? DefineTemplate 務(wù)必在 ReuseTemplate 之前使用
我們看到,createReusableTemplate 返回了一個 Tuple,即 define 和 reuse 的組件對,然后,通過上面的例子就可以在單文件里面復(fù)用多處代碼了。
還有,實際上還可以通過對象解構(gòu)的方式返回一個 define 和 reuse(很神奇吧,這篇文章就不展開了,有興趣下次再分享下),用法同上,例子如下
<script setup lang="ts">
const [DefineFoo, ReuseFoo] = createReusableTemplate<{ msg: string }>()
const TemplateBar = createReusableTemplate<{ msg: string }>()
const [DefineBiz, ReuseBiz] = createReusableTemplate<{ msg: string }>()
</script>
<template>
<DefineFoo v-slot="{ msg }">
<div>Foo: {{ msg }}</div>
</DefineFoo>
<ReuseFoo msg="world" />
<!-- 看這里 -->
<TemplateBar.define v-slot="{ msg }">
<div>Bar: {{ msg }}</div>
</TemplateBar.define>
<TemplateBar.reuse msg="world" />
<!-- Slots -->
<DefineBiz v-slot="{ msg, $slots }">
<div>Biz: {{ msg }}</div>
<component :is="$slots.default" />
</DefineBiz>
<ReuseBiz msg="reuse 1">
<div>This is a slot from Reuse</div>
</ReuseBiz>
<ReuseBiz msg="reuse 2">
<div>This is another one</div>
</ReuseBiz>
</template>

真是神奇,那咋實現(xiàn)呢
上面我們介紹了用法,相信應(yīng)該沒人看不懂,上手成本確實為 0,那接下來我們一起看看是如何實現(xiàn)的。
我們知道,Vue3 定義組件除了通過 script setup 的方式之外, 還可以通過defineComponent的方式
const Demo = defineComponent({
props: {
...,
},
setup(props, { attrs, slots }) {
return () => {
...
}
}
})
然后我們觀察下是如何定義模板的
<DefineFoo v-slot="{ msg }">
<div>Foo: {{ msg }}</div>
</DefineFoo>
好像似曾相識?v-slot?,誒,臥槽,這不是插槽嗎!還有,模板代碼看起來就是放在默認(rèn)插槽的。
好,我們接下來看下如何實現(xiàn) Define 的功能。
實現(xiàn) Define
我們剛才說,模板是定義在默認(rèn)插槽里面,那我們可以定義個局部變量 render,之后當(dāng)在 template 里面使用 Define 時會進入 setup,這時候拿到 slot.default,放在 render 上不就好?,代碼如下
let render: Slot | undefined
const Define = defineComponent({
setup(props, { slots, }) {
return () => {
/** 這里拿到默認(rèn)插槽的渲染函數(shù) */
render = slots.default
}
}
})
對的,就是這么簡單,對于 Define 來說,核心代碼就是這兩三行而已
實現(xiàn) Reuse
上面拿到 render function 了,那我們在使用 Reuse 的地方,將所得到的 v-slot、attrs 等傳給 render 不就好?
同樣,當(dāng)我們在 template 使用 Reuse 的時候,就會進入 setup,然后將得到的參數(shù)都傳給 render,并 return render 的結(jié)果即可
const reuse = defineComponent({
setup(_, { attrs, slots }) {
return () => {
/**
* 沒有render,有兩種可能
* 1. 你沒Define
* 2. Define寫在Reuse后面
**/
if (!render && process.env.NODE_ENV !== 'production')
throw new Error(`[vue-reuse-template] Failed to find the definition of template${name ? ` "${name}"` : ''}`)
return render?.({ ...attrs, $slots: slots })
}
},
})
上面的 attrs 也就是你在 Reuse 上傳的 prop 了
<ReuseFoo msg="msg1" />
而為啥還要傳個$slots?
上面實際上有個例子,模板里面還可以通過動態(tài)組件<component :is="$slots.default" />的方式來拿到插槽的真正內(nèi)容
<DefineBiz v-slot="{ msg, $slots }">
<div>Biz: {{ msg }}</div>
<component :is="$slots.default" />
</DefineBiz>
<ReuseBiz msg="reuse 1">
<div>This is a slot from Reuse</div>
</ReuseBiz>
<ReuseBiz msg="reuse 2">
<div>This is another one</div>
</ReuseBiz>

當(dāng)然,不只默認(rèn)插槽啦,其他具名插槽都可以
<DefineBiz v-slot="{ msg, $slots }">
<component :is="$slots.header" />
<div>Biz: {{ msg }}</div>
<component :is="$slots.default" />
</DefineBiz>
<ReuseBiz msg="reuse 1">
<template #header>
<div>我是 reuse1的header</div>
</template>
<div>This is a slot from Reuse</div>
</ReuseBiz>
<ReuseBiz msg="reuse 2">
<template #header>
<div>我是 reuse1的header</div>
</template>
<div>This is another one</div>
</ReuseBiz>

具體怎么玩出花,你來定~
類型支持,提升開發(fā)體驗
我們定義了模板,但模板接收什么參數(shù),傳入什么參數(shù),你得告訴我對不對,如果沒有類型的提示,那么開發(fā)體驗會極其糟糕,不過,不用擔(dān)心,大佬這些都考慮好了。
createReusableTemplate 支持泛型參數(shù),也就是說你要復(fù)用的模板需要什么參數(shù),只需要通過傳入對應(yīng)類型即可,比如你有個 msg,是 string 類型,那么用法如下
const [DefineFoo, ReuseFoo] = createReusableTemplate<{ msg: string }>()
然后你就會發(fā)現(xiàn),DefineFoo, ReuseFoo 都會對應(yīng)的類型提示了

添加類型支持
我們上面說是用 defineComponent 得到 Define 和 Reuse,而 defineComponent 返回的類型就是 DefineComponent 呀
type DefineComponent<PropsOrPropOptions = {}, RawBindings = {}, ...>
假設(shè)模板參數(shù)類型為 Bindings 的話,那么對于 Reuse 來說,其既支持傳參,也支持添加插槽內(nèi)容,所以類型如下
type ReuseTemplateComponent<
Bindings extends object,
Slots extends Record<string, Slot | undefined>,
/** Bindings使之有類型提示 */
> = DefineComponent<Bindings> & {
/** 插槽相關(guān) */
new(): { $slots: Slots }
}
而對于 Define 類型來說,我們知道其 v-slot 也有對應(yīng)的類型,且能通過$slots 拿到插槽內(nèi)容,所以類型如下
type DefineTemplateComponent<
Bindings extends object,
Slots extends Record<string, Slot | undefined>,
Props = {},
> = DefineComponent<Props> & {
new(): { $slots: { default(_: Bindings & { $slots: Slots }): any } }
}
小結(jié)一下
ok,相信我開頭說的看懂只需要 1 分鐘不到應(yīng)該不是吹的,確實實現(xiàn)很簡單,但功能又很好用,解決了無法在單文件復(fù)用代碼的問題。
我們來小結(jié)一下:
- 通過 Define 來將你所需要復(fù)用的代碼包起來,通過 v-slot 拿到傳過來的參數(shù),同時支持渲染其他插槽內(nèi)容
- 通過 Reuse 來復(fù)用代碼,通過傳參渲染出不同的內(nèi)容
- 為了提升開發(fā)體驗,加入泛型參數(shù),所以 Define 和 Reuse 都有對應(yīng)的參數(shù)類型提示
- 要記住使用條件,Define 在上,Reuse 在下,且不允許只使用 Reuse,因為拿不到 render function,所以會報錯
加個彩蛋吧
到了上面,實際上各位讀者已經(jīng)知道如何使用和實現(xiàn)了,但我比較喜歡瞎折騰,所以就對代碼稍稍魔改一下(大家這里看個樂就行,實際上多次調(diào)用 createReusableTemplate 得到相應(yīng)的 DefineXXX、ReuseXXX 具有更好的語義化)



也就是說,我不想多次調(diào)用 createReusableTemplate 了,直接讓 define 和 reuse 支持 name 參數(shù)(作為唯一的 template key),只要兩者都有相同的 name,那就意味著它們是同一對
如何魔改
實際上也很簡單,既然要支持 prop name來作為唯一的 template key,那 define 和 reuse 都添加 prop name 不就好?
const define = defineComponent({
props {
name: String
}
})
const reuse = defineComponent({
props {
name: String
}
})
然后之前不是有個 render 局部變量嗎?因為現(xiàn)在要讓一個 Define 支持通過 name 來區(qū)分不同的模板,那么我們判斷到傳入了 name,就映射對應(yīng)的的 render 不就好?
這里我們通過 Map 的方式存儲不同 name 對應(yīng)的 render,然后 define setup 的時候存入對應(yīng) name 的 render,reuse setup 的時候通過 name 拿到對應(yīng)的 render,當(dāng)然如果沒傳入 name,默認(rèn)值是 default,也就是跟沒有魔改之前是一樣的
const renderMap = new Map<string, Slot | undefined>()
const define = defineComponent({
props: {
/** template name */
name: String,
},
setup(props, { slots }) {
return () => {
const templateName: string = props.name || 'default'
if (!renderMap.has(templateName)) {
// render = slots.default
renderMap.set(templateName, slots.default)
}
}
},
})
const reuse = defineComponent({
inheritAttrs: false,
props: {
name: String,
},
setup(props, { attrs, slots }) {
return () => {
const templateName: string = props.name || 'default'
const render = renderMap.get(templateName)
if (!render && process.env.NODE_ENV !== 'production')
throw new Error(`[vue-reuse-template] Failed to find the definition of template${templateName}`)
return render?.({ ...attrs, $slots: slots })
}
},
})
最后
好了,以上就是給大家分享 Vue3 單文件如何復(fù)用代碼的過程了,確實很 hack 吧,至少我是這么覺得的。正所謂牛逼的代碼不在于量,而在于質(zhì),可稱之為真正的短小精悍。
最后我們來總結(jié)一下:
Vue3 單文件復(fù)用代碼只能通過抽組件的方式,但是如果只有幾行代碼的情況下,很多人就可能覺得麻煩,直接 CV 大法了。
所以這時候 createReusableTemplate 就橫空出世了,其通過定義模板的方式,在 setup 里面拿到 render function,然后在你復(fù)用代碼的地方調(diào)用該 render function 來實現(xiàn)代碼的復(fù)用。
當(dāng)然,需要注意的是,必須先定義模板,然后再復(fù)用模板,不然找不到對應(yīng)的 render function,代碼會報錯。
最后,我們又魔改了一下代碼,直接只使用一次 createReusableTemplate,通過 name 的方式來區(qū)分不同了的模板,當(dāng)然,還是推薦用魔改之前的用法,因為這種方式語義化不好。
以上就是想到頭禿也想不到的Vue3復(fù)用組件還可以這么hack的詳細(xì)內(nèi)容,更多關(guān)于Vue3復(fù)用組件的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
ant design vue嵌套表格及表格內(nèi)部編輯的用法說明
這篇文章主要介紹了ant design vue嵌套表格及表格內(nèi)部編輯的用法說明,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-10-10
vue中內(nèi)嵌iframe的src更新頁面未刷新問題及解決
這篇文章主要介紹了vue中內(nèi)嵌iframe的src更新頁面未刷新問題及解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-12-12
vue router+vuex實現(xiàn)首頁登錄驗證判斷邏輯
這篇文章主要介紹了vue router+vuex實現(xiàn)首頁登錄判斷邏輯,用于判斷是否登錄首頁,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-05-05
vue3使用vue-router及路由權(quán)限攔截方式
這篇文章主要介紹了vue3使用vue-router及路由權(quán)限攔截方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-04-04
Vue中Table組件行內(nèi)右鍵菜單實現(xiàn)方法(基于 vue + AntDesign)
這篇文章主要介紹了Vue中Table組件行內(nèi)右鍵菜單實現(xiàn)方法,該項目是基于 vue + AntDesign的,具體實例代碼給大家介紹的非常詳細(xì) ,需要的朋友可以參考下2019-11-11
Vue-Router實現(xiàn)組件間跳轉(zhuǎn)的三種方法
這篇文章主要為大家詳細(xì)介紹了Vue-Router來實現(xiàn)組件間跳轉(zhuǎn)的三種方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-11-11
vue中formdata傳值給后臺時參數(shù)為空的問題
這篇文章主要介紹了vue中formdata傳值給后臺時參數(shù)為空的問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-06-06

