"成功是由艰难的决策所铸就的。每次我们面临选择时,就像是站在十字路口,不同的路口通向不同的结果。我们需要睿智和勇气,去寻找那条最优的道路,因为只有通过最佳的选择,我们才能获得最好的结果。" - 贝尔纳德·肖(Bernard Shaw)
背景及问题描述
在许多业务场景中,我们需要在一个容器中展示多个子元素,这些子元素可能是各种不同类型的标签、文字、图片或其他内容。当子元素的数量过多,超出了容器的可见区域时,就需要考虑如何进行友好的展示,以免内容过载、换行或界面混乱。常见的问题包括:
-
如何在不浪费空间的前提下展示尽可能多的子元素?
-
如何在超出容器尺寸时进行内容省略,并提供用户查看更多的方式?
-
如何在不降低可用性和用户体验的情况下,优化多子元素的展示效果?
在接下来的部分,我们将介绍一些常见的友好展示方法,以及一些设计原则和最佳实践,帮助解决这些问题。
常见的友好展示方法
方式一:与产品和UI设计师沟通,进行子元素固定个数展示
运用栅格布局思想,在父级元素不同宽度的情况下展示对应的子元素个数,从而解决一行展示内容展示
优势:逻辑处理方便快捷
劣势:子元素之间的间距无法确保完全复原UI稿,会导致间距忽大忽小的情况,UI展示效果不够美观
方式二:基于DOM进行宽度计算
假设能拿到各个子元素的宽度,以及各个子元素之间的间距,将多个子元素宽度进行累加与父级宽度进行比较,比较后进行内容展示
优势:可以不浪费空间的前提下展示尽可能多的子元素,且不会出现异常情况,完全复原UI稿
劣势:需进行额外渲染,性能相比方式一会差一些
设计原则和最佳实践
易用性
在考虑实现这个功能时,首要问题是如何能让用户很便捷的使用它去实现这个效果,因此在易用性方面考虑如下设计:
JavaScript
<component props >
<元素1 />
<元素2 />
<元素3 />
<元素4 />
</component>
可扩展性
设计这个包裹性组件时需要考虑如下问题:
- 假设子元素过多,内容溢出,省略部分如何自定义?
此处就需要增加一个props,当内容溢出时,向外透出是从第几个元素开始溢出的以及省略了几个元素。因此我们进行props设计:
JavaScript
/**
* @param curIndex 当前从第几项开始省略,
* @param omitNum 子元素溢出父级时超出子元素的数量
* @returns 返回省略处自定义内容
*/
extraDomFun?: (curIndex: number, omitNum: number) => any;
- 假设用户考虑当前一行最多展示特定值,该如何处理?
我们也需进行一个props透传,增加如下参数:
JavaScript
/**
* @description 外部设置最多展示多少个子元素,注意,若该值大于父级所承载的,则按父级宽度计算
*/
maxCount?: number;
- 假设用户考虑展示固定个数子元素,是否可以满足?
同样我们增加props属性:
JavaScript
/**
* @description 是否以maxCount值固定展示多少个子元素
*/
isFocusCount?: Boolean;
用户反馈和交互
在上方这些props之外我们肯定也是需要进行样式及子元素的设定,方便研发同学在调用该组件时满足实际场景中的用户使用及交互,最终我们的props为:
JavaScript
interface IProps
extends React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> {
/**
* @description 组件包裹内容
*/
children: ReactNode;
/**
* @description 节点类名
*/
className?: string;
/**
* @param curIndex 当前从第几项开始省略,
* @param omitNum 子元素溢出父级时超出子元素的数量
* @returns 返回省略处自定义内容
*/
extraDomFun?: (curIndex: number, omitNum: number) => any;
/**
* @description 默认省略内容的Tooltip所配置的API
* @see https://overwatch.cn.goofy.app/docs/components/tooltip#tooltip
*/
tooltipProps?: TooltipProps;
/**
* @description 外部设置最多展示多少个子元素,注意,若该值大于父级所承载的,则按父级宽度计算
*/
maxCount?: number;
/**
* @description 是否以maxCount值固定展示多少个子元素
*/
isFocusCount?: Boolean;
}
设计实现
获取父级宽度
此处我们通过使用new ResizeObserver()方法去获取父级宽度,使用它的好处是我们也可以监听父级宽度变化,从而更友好的展示页面内容,提升用户体验,具体实现如下:
JavaScript
useEffect(() => {
const resizeObserver = new ResizeObserver(
debounce((entries, observer) => {
const entry = entries[0];
const { width, height } = entry.contentRect;
sizeSet({ width, height });
callback && callback(entries, observer);
}, 300)
);
// 监听div元素的大小变化
if (ref.current) {
resizeObserver.observe(ref.current);
}
return () => {
// 在组件卸载时停止监听
resizeObserver.disconnect();
};
}, [ref.current]);
获取子元素宽度及间距等
获取子元素
由于外层直接传递children
,因此我们就需要对children
进行处理,此处我们使用到React.Children.forEach(),将传入的children进行处理,获取到每个子元素,通过在浏览器中额外渲染这个子元素,即可获取该子元素的宽度,具体实现如下:
ini
/** 计算每个子元素的宽度 */
const calcChildrenWidth = (curChild: ReactElement) => {
const containerElement = createDom();
const root = createRoot(containerElement);
root.render(<BrowserRouter>{curChild}</BrowserRouter>);
/** 将容器元素添加到 body 中 */
document.body.appendChild(containerElement);
let curEleWidth = 0;
return new Promise((resolve) => {
requestIdleCallback(() => {
curEleWidth = Math.ceil(containerElement.offsetWidth);
document.body.removeChild(containerElement);
resolve(curEleWidth);
});
});
};
获取子元素间的间距
在日常工作中子元素的间距有可能是每个元素之间的margin,也有可能是父级设置的gap,因此,我们需要将两者都考虑进去,考虑时就需要将原本父级元素的样式也要加进来这样才能获取到真实的gap和margin。具体实现如下:
ini
/** 获取子元素间的边距或者gap */
const getMarginAndGap = async (allChildren: ReactElement, className = '') => {
const containerElement = document.createElement('div');
if (className) {
if (containerElement.classList) {
containerElement.classList.add(className);
} else {
containerElement.className = className;
}
}
const root = createRoot(containerElement);
root.render(allChildren);
/** 将容器元素添加到 body 中 */
document.body.appendChild(containerElement);
const getCurEleGap = new Promise((resolve) => {
requestIdleCallback(() => {
let rstNum = 0;
const childElements = containerElement.children;
for (let i = 1; i < childElements.length; i++) {
const prevChild = childElements[i - 1];
const currentChild = childElements[i];
const prevComputedStyle = window.getComputedStyle(prevChild);
const currentComputedStyle = window.getComputedStyle(currentChild);
const margin = Math.abs(
parseInt(currentComputedStyle.marginLeft, 10) - parseInt(prevComputedStyle.marginRight, 10)
);
const gap = currentChild.getBoundingClientRect().left - prevChild.getBoundingClientRect().right;
if (gap >= 0 && margin >= 0) {
rstNum = gap + margin;
break;
}
}
resolve(rstNum);
});
});
const curEleGap = (await getCurEleGap) || 0;
document.body.removeChild(containerElement);
return curEleGap;
};
比较多个子元素宽度和和父元素宽度
在比较时需考虑IProps中的maxCount进行边界处理,由于单纯的逻辑判断,此处就不再赘述
最终效果

注意事项与待改善
用户体验问题
通过上面的视线也可以发觉我们是使用了requestIdleCallback()去获取到元素的宽度,基于背景知识我们也可以理解到它是一个事件循环空闲时即将被调用的函数,因此在体感上会比页面其他内容渲染较慢
最后
在实现这个方案时,也考虑了其他的实现方式,例如使用canvas进行绘图等等。经过综合考虑,选择了当前的方案,但它仍有许多改进的空间。本文同时也是一个抛砖引玉的作用,以激发更好的解决方案的探讨,从而提高我们在实际工作中的效率。最后,值得一提的是,我们即将迎来2024新年,在这里祝愿大家新年大吉,龙年腾飞!