在vue的组件化开发过程中,透传几乎必不可少,在创建高级组件时非常有用。而透传类型可以分为三类:属性透传、事件透传、以及插槽透传。他们分别对应了$attrs
、$listeners
、$slots/$scopedSlots
。
属性和事件的透传想必大家非常熟悉,我们常用v-bind="$attrs"
和v-on="$listeners"
来透传属性和事件,详见官方文档「vm.\$attrs」与「vm.\$listeners」的用法说明。但说到插槽透传,除了手写对应插槽名称,其实还可以有更优雅的处理方式。
本文主要讲解在vue2中如何使用jsx编写组件,所以开始之前请务必了解渲染函数的数据对象结构,部分场景也会给出模板写法实例。至于vue3部分的插槽透传,可以参考$scopedSlots
的用法。
场景还原
首先我们基于BaseInput
组件开发了一个CustomInput
组件。
const BaseInput = { name: 'BaseInput', props: ['value'], render() { return ( <div class="base-input"> <span class="prefix">{this.$scopedSlots.prefix?.()}</span> <input value={this.value} onInput={e => this.$emit('input', e.target.value)} /> <span class="suffix">{this.$scopedSlots.suffix?.()}</span> </div> ); },};
我们通过CustomInput组件为BaseInput组件定制了样式,并且想在使用CustomInput组件时,手动传入BaseInput组件所需的prefix
和suffix
插槽。想实现这样的需求,通常我们会在CustomInput中这样写:
const CustomInput = { name: 'CustomInput', render() { return ( <BaseInput class="custom-input" {...{ attrs: this.$attrs, on: this.$listeners, }} > <template slot="prefix"> {this.$scopedSlots.prefix?.()} </template> <template slot="suffix"> {this.$scopedSlots.suffix?.()} </template> </BaseInput> ); },};
模板写法等价为
<template> <BaseInput class="custom-input" v-bind="$attrs" v-on="$listeners" > <slot name="prefix" slot="prefix"> <slot name="suffix" slot="suffix"> </BaseInput></template>
这样虽然可以实现需求,但是一旦BaseInput组件的插槽数量增加,我们就不得不在CustomInput中再穷举一遍,很明显,这对于CustomInput组件的维护来说并不友好,$attrs
与$listeners
同理。我们只是在BaseInput组件基础上定制了一点小功能,除此之外只是想把CustomInput组件当做BaseInput来用的。
那么有没有什么办法可以像透传属性和事件一样轻松来透传插槽呢?这样一来,BaseInput增加API时CustomInput就可以自动继承,无需修改了。
\$slots和\$scopedSlots的区别
上文中在使用jsx编写插槽代码时统一采用了$scopedSlots
API而非$slots
,这其实是有原因的。且看官方文档中关于\$scopedSlots API的描述。
2.6版本之后,所有的 $slots
现在都会作为函数暴露在 $scopedSlots
中。如果你在使用渲染函数,不论当前插槽是否带有作用域,我们都推荐始终通过 $scopedSlots
访问它们。这不仅仅使得在未来添加作用域变得简单,也可以让你最终轻松迁移到所有插槽都是函数的 Vue 3。
具体的暴露方式可参见下方源码部分:
......// expose normal slots on scopedSlotsfor (const key in normalSlots) { if (!(key in res)) { res[key] = proxyNormalSlot(normalSlots, key) }}......function proxyNormalSlot(slots, key) { return () => slots[key]}
两个重点。第一,这使得我们为插槽添加作用域变的简单;
<template> <BaseInput v-bind="$attrs" v-on="$listeners"> <template #prefix> <span>不需要作用域时插槽时可以这么写</span> </template> <template #prefix="{ value }"> <span>需要作用域时插槽时也可快速增加,例如这里的value {{ value }}</span> </template> </CustomInput></template>
加之所有的 $slots
都会作为函数暴露在 $scopedSlots
中,我们最初编写插槽时可以直接使用$scopedSlots
并传入参数,是否使用全凭使用者决定,极具灵活性。
第二,面向未来编程,便于迁移至vue3版本。在Vue3版本中,所有的插槽均作为函数暴露在$slots
上,如果我们现在开始使用$scopedSlots
,将来如果需要迁移时插槽部分只需要进行简单的全局替换即可,非常方便省事,没有副作用。
有了上面的基础,我们的CustomInput组件迎来升级,通过渲染函数直接传入$scopedSlots
,如此一来,传递给CustomInput组件的所有属性、事件、插槽都会原样传递给BaseInput组件,CustomInput组件就好像不存在一样。
const CustomInput = { name: 'CustomInput', render() { return ( <BaseInput class="custom-input" {...{ attrs: this.$attrs, on: this.$listeners, scopedSlots: this.$scopedSlots, // 新增 }} /> ); },};
兼容性
虽然全部使用$scopedSlots
的愿景很美好,但或许因为历史原因,我们使用的基础组件库中,并非所有组件统一使用$scopedSlots
语法,相当一部分组件仍在使用$slots
。虽然$slots
中的内容均会在$scopedSlots
中暴露一个函数与之对应,但反之却并没有这个联系。
假设我们的BaseInput组件全部使用this.$slots[name]
的方式调用插槽,而我们在CustomInput中间层组件中只传递了$scopedSlots
,这种情况下,BaseInput的将无法获取到$slots
,原因如上。所以CustomInput中间层组件还需要将自身的$slots
通过children的方式传递给BaseInput以实现透传,如下:
const CustomInput = { name: 'CustomInput', render() { return ( <BaseInput class="custom-input" {...{ attrs: this.$attrs, on: this.$listeners, scopedSlots: this.$scopedSlots, }} > {/* 新增 */} {Object.keys(this.$slots).map(name => ( <template slot={name}> {this.$slots[name]} </template> ))} </BaseInput> ); },};
模板写法
由于template模板中无法向子组件传递scopedSlot参数,故只能通过v-for遍历$scopedSlots
对象,生成对应的模板,如下:
<template> <BaseInput v-bind="$attrs" v-on="$listeners"> <template v-for="(_, name) in $scopedSlots" v-slot:[name]="data"> <slot :name="name" v-bind="data"/> </template> </BaseInput></template>
基于上文提到的特性,所以我们直接在中间层CustomInput中使用v-for遍历$scopedSlots
并填充<slot/>
组件即可达到效果。
结论
根据上方提到的jsx和模板的对应写法,以及兼容性章节叙述,有以下结论:
如果接收方(BaseInput)内部使用模板方式编写组件,或在使用jsx时统一使用了$scopedSlots
API,那么我们封装二级组件(CustomInput)时使用jsx借助渲染函数的scopedSlots参数即可快速透传插槽。
如果接收方混用$slots
和$scopedSlots
并且中间层组件使用了jsx编写,那么透传时需要额外使用children的方式传递中间层自身的$slots
,以确保接收方可以正常拿到相关插槽。
当然了,无论接收方(BaseInput)组件如何编写插槽,我们都可以在中间层(CustomInput)通过模板方式一劳永逸地透传。但你说你就是想用jsx,那就需要弄清二者的区别。
vue3补充
在vue3中,所有的插槽都是函数,统一暴露在$slots
中,我们可以看做vue2的$scopedSlots
。
在jsx中的写法可以参照Vue3版本的babel-plugin-jsx,使用v-slots
指定传递对象即可。
const App = { setup() { const slots = { default: () => <div>A</div>, bar: () => <span>B</span>, }; return () => <A v-slots={slots} />; },};
模板写法则与Vue2相同,只不过v-for遍历的对象变成了$slots
,具体写法参见上文。
最后
合理利用透传可以大幅提升高级组件开发效率,同时也能降低组件的维护成本,用更少的代码却能实现更多的事情,并且还易于维护,何乐而不为。
原文:https://juejin.cn/post/7094858996103774245