从0到1搭建react组件库-Space篇

277 阅读3分钟

引言

前面的文章中,我们已经实现了组件库的基本结构,并完成了ButtonInput等组件的开发。在编写项目Demo文档时,通常需要为Demo组件添加间距和布局样式,以使文档更加美观。然而,每次都重新编写样式文件显然效率低下。因此,本文将实现一个组件库中不可或缺的Space组件,其主要功能是为包裹的子组件添加间距。

调研

  在实现Space组件之前,我们先调研了几个主流组件库的实现方式:

Ant Design

  • 实现方式:通过结合flex布局和gap属性,为子元素添加间距。
  • 优点:代码简洁,易于理解。
  • 缺点:某些旧版浏览器不支持flex布局,可能导致兼容性问题。

Arco Design

  • 实现方式:根据size属性计算margin值,并根据direction属性确定margin的方向,然后将这些值分配给各个子元素。
  • 优点:兼容性好,适用于大多数浏览器。
  • 缺点:需要额外处理末尾元素的margin值,代码稍显复杂。

Mantine

  • 实现方式:在子元素之间插入div元素,并为这些div元素设置宽度来实现间距。
  • 优点:实现简单,兼容性好。
  • 缺点:在大量DOM元素的场景下,可能会影响性能。

分析:

综合以上调研结果,三种实现方式各有优劣:

  • Ant Design的实现方式简洁,但可能存在兼容性问题。
  • Arco Design的实现方式兼容性好,但需要额外处理末尾元素的margin
  • Mantine的实现方式简单,但在大量DOM元素场景下可能影响性能。

考虑到现代浏览器对flex布局的支持已经相当广泛,我们选择Ant Design的实现方式,主要通过flex布局来实现Space组件。关于flex布局的详细讲解,可以参考阮一峰老师的博客

实现步骤:

  容器布局

  • 布局方式Space组件的容器采用inline-flex布局。
  • 样式控制:通过classNamestyle属性来控制wrap(是否换行)、direction(主轴方向)和align(侧轴对齐方式)。

  类名派生计算:

  // class name
  const prefix = getPrefix('space')
  const classNames = cls(
    className,
    prefix,
    {
      [`${prefix}-wrap`]: wrap,
      [`${prefix}-${align}`]: align,
      [`${prefix}-${direction}`]: direction,
    }
  )

  样式属性:

@import url("../../../styles/index.less");

@space-prefix-cls: ~'@{prefix}-space';

.align(@align) {
  .@{space-prefix-cls}-@{align} {
    align-items: ~'@{align}';
  }
}

.@{space-prefix-cls} {
  display: inline-flex;

  &-vertical {
    flex-direction: column;
  }

  &-wrap {
    flex-wrap: wrap;
  }

  .align(start);
  .align(center);
  .align(end);
  .align(baseline);
  
}
  • Size处理Space组件的size属性在设计中是枚举值,我们通过一个函数将其转换为具体的间距值,并支持数组形式。
  function getGap(_size?: SpaceSize) {
    if(isNumber(_size)) {
      return _size
    }
    
    switch(_size) {
      case "mini":
        return 4;
      case "small":
        return 8;
      case "middle":
        return 16;
      case "large":
        return 24;
      default:
        return 8
    }

  }

  function getGapStyle (_size: SpaceSize | SpaceSize[]): CSSProperties {
    if(isArray(_size)) {
      return {
        rowGap: getGap(_size[0]),
        columnGap: getGap(_size[1])
      }
    }
    return {
      gap: getGap(_size)
    }
  }

  子节点处理:

  • 子节点拍平:使用toArray方法拍平子节点,确保所有子节点(Fragment导致的嵌套场景, 空值)都能被正确处理。

    import React from "react";
    import { ReactNode } from "react";
    import { isFragment } from "react-is"
    
    export function toArray(nodes: ReactNode) {
     let result: ReactNode[] = []
    
     React.Children.forEach(nodes, (child) => {
       if (isFragment(child) && child?.props.children) {
         result = result.concat(toArray(child.props.children))
       } else if (child !== undefined && child !== null) {
         result.push(child)
       }
     })
    
     return result
    }
                   
    
  • 遍历生成:在遍历子节点时,根据是否为末尾元素决定是否添加Split切割符。

  • Key处理:通过读取ReactElementkey值,确保每个子元素都有唯一的key,避免因key重复导致的渲染问题。

      {
        childrenNodes.map((child, index) => {
          const key = (child as ReactElement)?.key || index
          return (
            <Fragment key={key}>
              <SpaceItem className={itemClassNames}>
                {child}
              </SpaceItem>
              {
                lastChildIndex !== index && split
                ? split
                : null
              }
            </Fragment>
          )
        })
      }
    

最终效果:

仓库地址