前言
无论是 vue 还是 react,当遇到多处反复代码的时候,咱们都会想着如何复用这些代码,而不是一个文件里充斥着一堆冗余代码。
实际上,vue 和 react 都能够通过抽组件的形式来达到复用,但如果遇到一些很小的代码片段,你又不想抽到另外一个文件的状况下,相比而言,react 能够在雷同文件外面申明对应的小组件,或者通过 render function 来实现,如:
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 一样在单个文件外面申明其余组件,如果你要复用代码,那就只能通过抽离组件的形式。
可是啊可是啊!就如下面 Demo 组件,就零零散散两三行代码,抽成一个组件你又感觉没太必要,那惟一的解决方案就是 CV 大法?(当然,如果是诸如 list 之类的,能够用 v-for 代码,但这里介绍的不是这种场景)
我晓得你很急,但你先别急。如果咱们能够通过在组件范畴内将要复用的模板圈起来,跟 vue 说,喂,这代码是我圈起来的,因为我有好几处要用到,尽管目前你看起来如同不反对这性能,不过,没事,你实现不了的,我来实现
那大抵实现计划就是这样子啦:
<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>
可是,这种计划到底如何实现呢?毕竟牛都曾经吹入地了,实现不了也要硬着头皮折腾。好了,不卖关子,antfu 大佬实际上曾经实现了,叫做createReusableTemplate
,且放到 VueUse 外面了,具体能够点击文档看看。
接下来我会给大家介绍下 createreusabletemplate 如何应用,以及是如何实现的(不必缓和,上手老本为 0,且看懂只须要 1 分钟不到)
用法
通过 createReusableTemplate 拿到定义模板和复用模板的组件
<script setup>import { createReusableTemplate } from '@vueuse/core'const [DefineTemplate, ReuseTemplate] = createReusableTemplate()</script>
而后在你要复用代码的中央,将其用DefineTemplate
包起来,之后就能够通过ReuseTemplate
在单文件 template 的任意中央应用了:
<template> <DefineTemplate> <!-- 这里定义你要复用的代码 --> </DefineTemplate> <!-- 在你要复用的中央应用ReuseTemplate, --> <ReuseTemplate /> ... <ReuseTemplate /></template>
⚠️ DefineTemplate 务必在 ReuseTemplate 之前应用
咱们看到,createReusableTemplate 返回了一个 Tuple,即 define 和 reuse 的组件对,而后,通过下面的例子就能够在单文件外面复用多处代码了。
还有,实际上还能够通过对象解构的形式返回一个 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>
真是神奇,那咋实现呢
下面咱们介绍了用法,置信应该没人看不懂,上手老本的确为 0,那接下来咱们一起看看是如何实现的。
咱们晓得,Vue3 定义组件除了通过 script setup 的形式之外, 还能够通过defineComponent
的形式
const Demo = defineComponent({ props: { ..., }, setup(props, { attrs, slots }) { return () => { ... } }})
而后咱们察看下是如何定义模板的
<DefineFoo v-slot="{ msg }"> <div>Foo: {{ msg }}</div></DefineFoo>
如同似曾相识?v-slot?,诶,卧槽,这不是插槽吗!还有,模板代码看起来就是放在默认插槽的。
好,咱们接下来看下如何实现 Define 的性能。
实现 Define
咱们方才说,模板是定义在默认插槽外面,那咱们能够定义个局部变量 render,之后当在 template 外面应用 Define 时会进入 setup,这时候拿到 slot.default,放在 render 上不就好?,代码如下
let render: Slot | undefinedconst Define = defineComponent({ setup(props, { slots, }) { return () => { /** 这里拿到默认插槽的渲染函数 */ render = slots.default } }})
对的,就是这么简略,对于 Define 来说,外围代码就是这两三行而已
实现 Reuse
下面拿到 render function 了,那咱们在应用 Reuse 的中央,将所失去的 v-slot、attrs 等传给 render 不就好?
同样,当咱们在 template 应用 Reuse 的时候,就会进入 setup,而后将失去的参数都传给 render,并 return render 的后果即可
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?
下面实际上有个例子,模板外面还能够通过动静组件<component :is="$slots.default" />
的形式来拿到插槽的真正内容
<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>
当然,不只默认插槽啦,其余具名插槽都能够
<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>
具体怎么玩出花,你来定~
类型反对,晋升开发体验
咱们定义了模板,但模板接管什么参数,传入什么参数,你得通知我对不对,如果没有类型的提醒,那么开发体验会极其蹩脚,不过,不必放心,大佬这些都思考好了。
createReusableTemplate 反对泛型参数,也就是说你要复用的模板须要什么参数,只须要通过传入对应类型即可,比方你有个 msg,是 string 类型,那么用法如下
const [DefineFoo, ReuseFoo] = createReusableTemplate<{ msg: string }>()
而后你就会发现,DefineFoo, ReuseFoo 都会对应的类型提醒了
增加类型反对
咱们下面说是用 defineComponent 失去 Define 和 Reuse,而 defineComponent 返回的类型就是 DefineComponent 呀
type DefineComponent<PropsOrPropOptions = {}, RawBindings = {}, ...>
假如模板参数类型为 Bindings 的话,那么对于 Reuse 来说,其即反对传参,也反对增加插槽内容,所以类型如下
type ReuseTemplateComponent< Bindings extends object, Slots extends Record<string, Slot | undefined>, /** Bindings使之有类型提醒 */> = DefineComponent<Bindings> & { /** 插槽相干 */ new(): { $slots: Slots }}
而对于 Define 类型来说,咱们晓得其 v-slot 也有对应的类型,且能通过$slots 拿到插槽内容,所以类型如下
type DefineTemplateComponent< Bindings extends object, Slots extends Record<string, Slot | undefined>, Props = {},> = DefineComponent<Props> & { new(): { $slots: { default(_: Bindings & { $slots: Slots }): any } }}
小结一下
ok,置信我结尾说的看懂只须要 1 分钟不到应该不是吹的,的确实现很简略,但性能又很好用,解决了无奈在单文件复用代码的问题。
咱们来小结一下:
- 通过 Define 来将你所须要复用的代码包起来,通过 v-slot 拿到传过来的参数,同时反对渲染其余插槽内容
- 通过 Reuse 来复用代码,通过传参渲染出不同的内容
- 为了晋升开发体验,退出泛型参数,所以 Define 和 Reuse 都有对应的参数类型提醒
- 要记住应用条件,Define 在上,Reuse 在下,且不容许只应用 Reuse,因为拿不到 render function,所以会报错
加个彩蛋吧
到了下面,实际上各位读者曾经晓得如何应用和实现了,但我比拟喜爱瞎折腾,所以就对代码稍稍魔改一下(大家这里看个乐就行,实际上屡次调用 createReusableTemplate 失去相应的 DefineXXX、ReuseXXX 具备更好的语义化)
也就是说,我不想屡次调用 createReusableTemplate 了,间接让 define 和 reuse 反对 name 参数(作为惟一的 template key),只有两者都有雷同的 name,那就意味着它们是同一对
如何魔改
实际上也很简略,既然要反对 prop name
来作为惟一的 template key,那 define 和 reuse 都增加 prop name 不就好?
const define = defineComponent({ props { name: String } }) const reuse = defineComponent({ props { name: String } })
而后之前不是有个 render 局部变量吗?因为当初要让一个 Define 反对通过 name 来辨别不同的模板,那么咱们判断到传入了 name,就映射对应的的 render 不就好?
这里咱们通过 Map 的形式存储不同 name 对应的 render,而后 define setup 的时候存入对应 name 的 render,reuse setup 的时候通过 name 拿到对应的 render,当然如果没传入 name,默认值是 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 单文件如何复用代码的过程了,的确很 hack 吧,至多我是这么感觉的。正所谓牛逼的代码不在于量,而在于质,可称之为真正的短小精悍。
最初咱们来总结一下:
Vue3 单文件复用代码只能通过抽组件的形式,然而如果只有几行代码的状况下,很多人就可能感觉麻烦,间接 CV 大法了。
所以这时候 createReusableTemplate 就横空出世了,其通过定义模板的形式,在 setup 外面拿到 render function,而后在你复用代码的中央调用该 render function 来实现代码的复用。
当然,须要留神的是,必须先定义模板,而后再复用模板,不然找不到对应的 render function,代码会报错。
最初,咱们又魔改了一下代码,间接只应用一次 createReusableTemplate,通过 name 的形式来辨别不同了的模板,当然,还是举荐用魔改之前的用法,因为这种形式语义化不好。
好啦,本文到此结束,谢谢各位看官,感觉不错的话还请点个赞再走,谢谢啦~