手把手,跟着element-plus学Space组件

1,619 阅读3分钟

手把手系列文章,仿element-plus框架封装一套自己的组件库。

学习element-plus框架源码,有助于我们更好的理解组件设计,加深对vue的理解。

今天要写的是Space组件。

1.环境准备

克隆element-plus代码到本地

git clone https://github.com/element-plus/element-plus
cd element-plus
pnpm i
pnpm run docs:dev

当然我更推荐用我的这个组件库做调试,既实现了组件的基本功能,代码复杂性也会小很多。

git clone https://github.com/bubuui/bubu-ui
cd bubu-ui
npm i 
npm run docs:dev

2.需求分析

开始阅读源码之前,先分析一波需求。

先看最基础的用法。

<el-space wrap>
    <el-button>1<el-button>
    <el-button>1<el-button>
</el-space>

组件需要提供一个容器,然后设置flex布局,可以根据el-space上的属性,来动态控制是否换行,水平排列还是垂直排列。

看起来,好像只需要提供一个容器组件就行。

但是,如果有size,好像不太好实现了。

<el-space :size="20">
    <el-button>1<el-button>
    <el-button>1<el-button>
</el-space>

因为我们只有容器,size需要设置子元素的样式。

那么怎么操作子元素呢?

是不是可以像form组件一样,再封装一个space-item组件呢?

我们来看看element-plus是怎么实现的。

3.阅读源码

从目录的文件就可以看出来,还真有item。

而且没有.vue文件,那大概率就是通过api创建的组件。

image.png

先找入口文件——index.ts。

虽然有Item,但是注册的只有Space组件。(withInstall就是提供一个install方法,并使用app.component注册组件)

image.png

我们继续看-Space.ts

image.png

renderSlot渲染插槽,该函数的作用是渲染slots里面的default插槽内容。

返回的children是一个Fragment虚拟节点,子节点内容为default插槽数组。

这么说有点抽象,我们看一下这个函数的定义。

image.png

image.png

其实就是取出slots里面的default插槽。

相当于如下代码

   const children = createBlock(Fragment, {key: 0}, slots['default']?.(props))

了解了这个函数之后,我们继续看extractChildren这个函数。

创建了一个虚拟节点,并把child作为默认插槽传入。

image.png

Item组件就很简单了,就是渲染一下默认插槽。

image.png

也就是说,Space给我们自动包了一层Item组件。

<el-space :size="20">
    <el-button>1<el-button>
    <el-button>1<el-button>
</el-space>

// 实际渲染的是

<el-space :size="20">
    <item>
        <el-button>1<el-button>
    </item>
    <item>
        <el-button>1<el-button>
    </item>
</el-space>

ps(有些人可能会奇怪,为什么一会用h函数,一会用createVNode函数,其实h函数内部调用的也是createVNode,只不过h函数加了一些参数处理)

4.手写Space组件

手写的思路跟element-plus是一样的。

唯一的区别是,我是用tsx语法写的。

组件实现了基本功能,ts类型检测单独提取一个文件,看上去会直观一些。

export default defineComponent({
  name: 'BuSpace',
  props: sapceProps,
  setup(props: SapceProps, { slots }) {
    const { direction, wrap, alignment, size } = toRefs(props);
    const prefixCls = 'bu-space';
    const classes = computed(() => [
      prefixCls,
      `${prefixCls}--${direction.value}`,
    ]);
    const sizeSpace = computed(() => {
      return size.value
        ? typeof size.value === 'string'
          ? size.value
          : size.value + 'px'
        : '8px';
    });
    return () => {
      const children = renderSlot(slots, 'default', { key: 0 }, () => []);
      if ((children.children ?? []).length === 0) return null;
      return (
        <div
          class={classes.value}
          style={{
            'flex-wrap': wrap.value ? 'wrap' : 'nowrap',
            'align-items': alignment.value,
          }}
        >
          {isArray(children.children) &&
            children.children.map((item: any) => {
              if (item.type === Comment) {
                return h(item);
              } else {
                return h(
                  'div',
                  {
                    class: 'bu-space--item',
                    style: {
                      'margin-bottom':
                        direction.value === 'horizontal' ? 0 : sizeSpace.value,
                      'margin-right':
                        direction.value === 'horizontal' ? sizeSpace.value : 0,
                    },
                  },
                  item
                );
              }
            })}
        </div>
      );
    };
  },
});

总结一下

space组件功能很简单,但是内部实现细节还是挺多的。

由于需要对默认插槽做包装,所以通过手动渲染,调用api的方式进行组件封装。(h函数,createVnode函数)

学习了renderSlot函数的使用。(这个api在官方文档好像并没有介绍)

也通过tsx语法,手写了一个Space组件,并实现了基本功能。