实现一个vue3组件库 - partial挑选(抽象组件)


theme: vue-pro

9.jpg
pid: 109362778

引言

当我们在实现某个组件需要获取到外部传入的slot的引用、或者希望slot是单个元素时,通常会包裹一层元素(比如div)通过给外层元素打上ref标记来间接获取,例如:

<template>
    <divider ref="container">
       <slot></slot>
    </divider>


</template>

缺点很明显,加了一层元素破坏了原本的html结构

假设slot本身就是单个元素或者希望slot是单个元素时,如何在组件内部不添加外层元素的情况下,拿到slot单元素的引用呢?
比如:

<s-partial ref="default">
    <slot></slot>
</s-partial>

通过default标记,我们可以拿到slot中的第一个合法元素,且s-partial不会添加任何dom元素。

举个具体的例子,比如在使用Popconfirm 气泡确认框 | Element Plus 时,就会默认使得reference slot是单个元素.

但是当我们传入多个元素时, ELPopconfirm也能在不添加外层div的情况下,实现效果:

<el-popconfirm title="Are you sure to delete this?">
 <template #reference>
  <el-button>Delete</el-button>
  <el-button>Delete</el-button>
 </template>
</el-popconfirm>

image.png

可以看到我们传入的reference元素时两个按钮,但是实际只渲染了第一个元素,也就是从传入的slot中挑选出了第一个元素作为reference

为此,我们可以尝试实现一个抽象组件partial来实现上述功能。

前置芝士

何谓抽象组件?

在 Vue 中,抽象组件(Abstract Components)是一种特殊的组件,它们不会渲染成任何实际的 DOM 元素。抽象组件通常用于包装其他组件,以提供一些额外的逻辑、功能或样式,而不在页面上生成额外的 DOM 结构。

和vue文件写法不一样,抽象组件一般不携带template模块,抽象组件更像是由一堆js/ts组成的逻辑代码。最基本的抽象组件代码如下:

export default defineComponent({
    setup(props, context) {
    
       return () => {
        return h(??)
       }

    },

})

其中setup函数的参数,props为你理解的props, context为当前组件的上下文。通过上下文,你可以拿到许多有用的信息,比如slots, attrs, 当然对于很多信息vue也提供相应的hook来获取他们,比如useSlots, useAttrs

其中setup函数的返回值被称为render函数, 用作vue的渲染过程,
render函数的返回值应该是一个h函数,也被称为createElement函数, 此函数用作vue中创建虚拟节点VNode

h(createElement)函数

简单来说: h函数可以接受三个参数h( tag: string|VNode, props: {}, children: [] )

其中tag表示虚拟节点的类型:

  • 当它是string时,取值可以是div span这些普通的dom节点名称, 也可以是你定义或全局组件的名称
  • 当它是VNode类型时, 返回它本身

props表示给这个虚拟节点加上的属性,注意,并不是组件的props,而是像是attribute,比如class, style, data-x这些给节点加上的属性

children表示这个虚拟节点的子节点, 可以是h函数的返回值(返回VNode),也可以是现成的VNode(比如你传入的插槽)

如何使用slots?

当我们在使用某个组件时,难免会需要传入一些slot, 其实这些slot都是有名字的,也就是具名插槽 默认的名字为default 因此我们可以通过slots.default(slotName)来获取对应的插槽。

需要注意的是, slots.default(slotName)是一个函数,这些函数的返回值才是你真正传入的模板生成的VNodeList

因此,当我们写一个抽象组件,想要给传入的模板套一层div再渲染时,可以写成这样:

<script lang="ts">

import {defineComponent, h, useAttrs, useSlots, warn} from "vue";

export default defineComponent({
    setup(props, context) {
       const slots = context.slots;
       const attrs = context.attrs;
       /**
        * 或者写成
        * const slots = useSlots();
        * const attrs = useAttrs();
        */

       return () => {
          return h('div', {...attrs}, [slots.default()])
       }
    }


})
</script>

实现思路

再来想想我们需要的效果, 获取传入slot.default中模板的第一个元素节点。

需要注意的是,vue会把注释节点、文本节点、template、slot节点都渲染成VNode

这些节点对于我们来说就是干扰项,需要排除。

如何区分不同的VNode呢?

我们先写一段代码来看看:

<script lang="ts">

import {defineComponent, h, VNode} from "vue";
export default defineComponent({
    setup(__, context) {
       const slots = context.slots;
       const attrs = context.attrs;

       const VNodeList:VNode[] = slots.default && slots.default();

       for (let i = 0; i < VNodeList.length; i++) {
          const VNode = VNodeList[i];
          console.log(VNode,"type: " ,VNode.type);
       }

       return () => {
          return h('div', {...attrs}, [slots.default()])
       }


    },
    name:'SPlay'


})
</script>

上述代码只是打印出slot.default下虚拟节点的类型

测试:

<s-play>
    <template #default>
       我是文本节点
       <!--我是注释节点-->
       <s-input placeholder="我是组件节点"></s-input>
       <input placeholder="我是普通dom节点"/>
       <slot>我是slot节点</slot>
    </template>

</s-play>

我们不管渲染效果是什么,打印的结果是:

image.png

可以看到对于特殊的节点,它们的type都是symbol类型的。这是一个很重要的区分标志。

代码实现

满足我们需求的节点有: 文本节点, 组件节点,普通dom节点。明确这点之后,我们就很容易写出以下代码:

<script lang="ts">

import {defineComponent, h, useAttrs, VNode, warn} from "vue";


export default defineComponent({
    setup(__, context) {
       const slots = context.slots;
       const attrs = useAttrs();

       // slots.default为空, 抛出警告
       if (!slots.default) {
          warn('SPartial has empty slot');
          return void 0
       }

       const VNodes: VNode[] = slots.default();
       let dft: any;  //default, 目标节点
       let old: any;  //目标节点的父节点


       // 遍历default函数生成的虚拟节点列表, 找到第一个非注释节点
       for (let i = 0; i < VNodes.length; i++) {
          const VNode = VNodes[i];
          if (VNode.type !== Symbol.for('v-cmt')) {
             dft = VNode;
             break;
          }

       }

       // 经过遍历后,如果值为空,说明没有满足情况的节点,抛出警告
       if (!dft) {
          warn("is a empty nodeList!");
          return void 0
       }


       // 只有当节点类型是template、slot、txt、comment时, type才会是symbol
       // 这里是为了找到第一个满足情况的节点
       while (dft && typeof dft!.type === 'symbol') {
          let index = 0;

          // 当前是文本节点,满足要求, 为当前文本节点套上一层span
          if (dft!.type === Symbol.for('v-txt')) {
             dft = h('span', {...attrs}, [dft]);
             break;
          }
          // 当前节点是注释节点,则不断获取该节点的下一个节点
          while (dft!.type === Symbol.for('v-cmt') && index < old.children.length) {
             dft = (old as any)!.children[++index];

          }

          // 没有经历上面的过程则index不变, 此时只剩template和slot的情况
          // 获取当前节点的第一个子节点
          if (index === 0) {
             old = dft;
             dft = (dft as any)!.children[0];
          }


       }

       // 如果到最后也没有合法节点,则抛出警告
       if (!dft) {
          warn("There are no available VNodes for partial");
          return
       }


       // 最终渲染该节点
       return () => {
          return h(dft as any, {...attrs})
       }


    },
    name: 'SPartial',


})
</script>

最终效果

  • egs1:

        <s-partial>
    <!--       cmt 1-->
    <!--       cmt 2-->
           <slot>
    <!--          cmt 3-->
              <s-input></s-input>
           </slot>
    
        </s-partial>
    

    image.png

  • egs2:

        <s-partial>
    <!--       cmt 1-->
           hello world!
    <!--       cmt 2-->
           <slot>
    <!--          cmt 3-->
              <s-input></s-input>
           </slot>
    
        </s-partial>
    

    image.png

写在最后

这里只是提供引言中所述问题的一个解决方案, 代码可能不全面, 欢迎在评论区指出,一起掉头发(

这个项目的地址是:lastertd/sss-ui-plus: 适用于vue3的组件库 (github.com)在这里求一个star✨

感谢看到最后💟💟💟

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值