实际上,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 拿到定义模板和复用模板的组件
<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复用组件怎么使用的详细内容。
