引言
前面的文章中,我们已经实现了组件库的基本结构,并完成了Button、Input等组件的开发。在编写项目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布局。 - 样式控制:通过
className和style属性来控制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处理:通过读取
ReactElement的key值,确保每个子元素都有唯一的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> ) }) }