首页>>前端>>Vue->如何在Vue2/3中正确透传插槽,提升组件编写效率?

如何在Vue2/3中正确透传插槽,提升组件编写效率?

时间:2023-11-30 本站 点击:1

在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组件所需的prefixsuffix插槽。想实现这样的需求,通常我们会在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编写插槽代码时统一采用了$scopedSlotsAPI而非$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时统一使用了$scopedSlotsAPI,那么我们封装二级组件(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


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:/Vue/3739.html