需求分析
▐ 需要支持模块多种排布形式
通过对现有项目汇总分析,根据feeds流中填充模块高度是否固定、排布方式列数不同可以划分为:一排一定高、一排一不定高、一排二定高、一排二不定高。
一排一定高:高度固定, 模块依次向下排列
一排一不定高:不通类型模块高度不固定, 模块依次向下排列
一排二定高:高度一致, 两列排列
一排二不定高:不同类型模块高度不固定, 模块总是排布在最短的一侧
▐ 需要支持不同类型数据, 渲染不同模块
feeds中一般会穿插多种类型数据,需要针对不同的数据,有不同的UI展示,比如下面feeds中,有图文、商品、视频。组件内默认每个模块存在type字段,进行不同类型区分。
▐ 针对性能优化,组件内置虚拟滚动
当页面滚动时,会不断加载新的feeds数据,将数据渲染在页面上,这必然会在页面中渲染大量的DOM节点,当页面中DOM数量超过一定数值,数据变更导致页面重新渲染时,会造成页面的卡顿,常见的feeds场景下针对大量DOM节点的优化方案是虚拟滚动,所以需要这一性能优化,集成在feeds组件中。
实现细节
▐ 排布方式不一致实现
- 高度问题解决方案
如果模块固定高度,则直接传入750设计稿下的模块高度即可
如果模块不定高,则需要按照模块类型不同,传入当前类型模块的高度计算规则 - 排列方式解决方案
为了兼容一排二不定高情况下,模块总是排列在最小高度的一边,所以模块的排列全部使用定位的方式进行模块布局
模块是一排一或者一排二,使用组件时,传入对应类型即可
- 组件使用方法
<FeedsComponent mode="grid" // fixed 一排一 grid一排二 itemSize={(item) => { if(item === 'video') { // .... video场景下高度计算逻辑 } else if(item === 'banner') { // .... banner场景下高度固定300 return 300 } else { return xxx } }} ></FeedsComponent>
- 核心实现代码
const calculateItemStyle = function (componentStyles, data, { itemSize = undefined, model = 'fixed' } = {}) { if (typeof itemSize === 'undefined') throw new Error('itemSize 为函数或者数字'); if (model === 'fixed') { // 单列 data.forEach(item => { const size = typeof itemSize === 'function' ? itemSize(item) : itemSize; const lastItemSize = componentStyles[componentStyles.length - 1] || { top: 0, height: 0 }; componentStyles[componentStyles.length] = { left: 0, top: lastItemSize.top + lastItemSize.height, height: size, width: '100%', }; }); } else { // 多列 const calculateGridPosition = (function () { // 计算多列情况下, 页面布局 let leftFeeds = 0; // 变量命名 let rightFeeds = 0; const stylesLength = componentStyles.length; if (stylesLength > 0) { // 说明已经存在数据 const lastStyle = componentStyles[stylesLength - 1]; if (lastStyle.direction === 'left') { // 说明最后一个是在左边 leftFeeds = lastStyle.top + lastStyle.height; } else { rightFeeds = lastStyle.top + lastStyle.height; } // ..... } return (styles, size) => { let itemStyle = null; if (leftFeeds <= rightFeeds) { // 左边小, 放左边 itemStyle = { left: '0', top: leftFeeds, direction: 'left' }; leftFeeds += size; } else { itemStyle = { left: '50%', top: rightFeeds, direction: 'right' }; rightFeeds += size; } styles[styles.length] = { left: itemStyle.left || undefined, top: itemStyle.top, height: size, width: '100%', direction: itemStyle.direction, }; }; }()); data.forEach(item => { const size = typeof itemSize === 'function' ? itemSize(item) : itemSize; calculateGridPosition(componentStyles, size); }); } return componentStyles; };
支持不同类型数据, 渲染不同组件能力
提供组件注册能力,只需要将组件注入到feedsComponent中,组件内会按照数据不同,渲染对应类型的模块。
- 组件使用方法
feedsComponent.registerComponent({ video: Video, item: Item })
- 核心代码实现
let feedsItemMap = {} function Feeds(props: Props) {} Feeds.registerItemComponent = function (componentMap: { [key: string]: any }) { feedsItemMap = componentMap; }; <div class="feeds-container"> { data.map(item => createElement(feedsItemMap[item.type], { ...item, index, }) }) } </div>
▐ 虚拟滚动实现
用户滑动页面的过程中,获取当前滚动高度,结合《排布方式不一致实现》章节中,在数据初始化时已经计算好模块应该排布的位置,找到当前可视区域内符合条件的开始和结束的index,渲染在页面中。
- 核心代码实现
useEffct(() => { const onScroll = throttle(() => { const { scrollTop, clientHeight, scrollHeight } = document.documentElement; // 根据当前页面scrollInfo, 获取显示数据的startIndex和endIndex const { startIndex, endIndex } = getRenderDataIndex(feedsItemStyles.current, scrollInfo); // ... }, 200) window.addEventListener('scroll', onScroll); }, []) // utils/index.js const findTargetIndex = function (styles, target) { // 二分法 找到大致接近的top的元素index const n = styles.length; let left = 0; let right = n - 1; let ans = n; while (left <= right) { let mid = (Math.floor((right - left) / 2)) + left; if (target <= styles[mid].top) { ans = mid; right = mid - 1; } else { left = mid + 1; } } return ans; }; const getRenderDataIndex = (feedsStyles, scrollInfo) => { const {scrollTop, clientHeight, scrollHeight} = scrollInfo; const start = scrollTop < 0 ? 0 : scrollTop; // 开始位置 let end = 0; // 结束位置 if ((scrollTop + clientHeight) > scrollHeight) { // 说明没有东西了 end = scrollHeight; } else { end = scrollTop + clientHeight; } let startIndex = 0; let endIndex = 0; if (start === 0) { // 说明是顶了 startIndex = 0; } else { const targetIndex = findTargetIndex(feedsStyles, start) - 5; startIndex = targetIndex < 0 ? 0 : targetIndex; // 向上多 } }
总结
通过对feeds使用场景梳理,确定组件需要具备的能力,并将性能优化、feeds模块排布这些通用逻辑内置的组件内部,通过参数传入选择场景,组件注册注入数据对应的展示模块,方便开发者使用,提高开发效率。