我们看到,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 | undefined
const 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 () => {
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>,
> = 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: {
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复用组件怎么使用的详细内容,更多请关注编程网其它相关文章!