通用feeds组件封装技巧

简介: 前端交互中,无限滚动feeds流是最常见的交互形式,常见的feeds功能上基本都包括兜底、接口请求、报错提示、刷新、feeds卡片排列规则等。用户滑动页面不断加载feeds数据, DOM节点不断累加创建,因为DOM节点过多,导致页面数据更新时卡顿问题, 一般会通过虚拟滚动减少页面DOM数量来解决。这些必要基础能力在每个feeds流需求中都存在,所以我们决定将feeds这一场景抽离成公共组件,提高开发效率。

需求分析

 需要支持模块多种排布形式


通过对现有项目汇总分析,根据feeds流中填充模块高度是否固定、排布方式列数不同可以划分为:一排一定高、一排一不定高、一排二定高、一排二不定高。

image.png

一排一定高:高度固定, 模块依次向下排列

一排一不定高:不通类型模块高度不固定, 模块依次向下排列

一排二定高:高度一致, 两列排列

一排二不定高不同类型模块高度不固定, 模块总是排布在最短的一侧

 需要支持不同类型数据, 渲染不同模块

feeds中一般会穿插多种类型数据,需要针对不同的数据,有不同的UI展示,比如下面feeds中,有图文、商品、视频。组件内默认每个模块存在type字段,进行不同类型区分。

image.png

 针对性能优化,组件内置虚拟滚动


当页面滚动时,会不断加载新的feeds数据将数据渲染在页面上这必然会在页面中渲染大量的DOM节点当页面中DOM数量超过一定数值数据变更导致页面重新渲染时会造成页面的卡顿常见的feeds场景下针对大量DOM节点的优化方案是虚拟滚动所以需要这一性能优化集成在feeds组件中。


实现细节

 排布方式不一致实现


  1. 高度问题解决方案
    如果模块固定高度,则直接传入750设计稿下的模块高度即可
    如果模块不定高,则需要按照模块类型不同,传入当前类型模块的高度计算规则
  2. 排列方式解决方案
    为了兼容一排二不定高情况下,模块总是排列在最小高度的一边,所以模块的排列全部使用定位的方式进行模块布局
    模块是一排一或者一排二,使用组件时,传入对应类型即可


  • 组件使用方法
<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模块排布这些通用逻辑内置的组件内部,通过参数传入选择场景,组件注册注入数据对应的展示模块,方便开发者使用,提高开发效率。


相关文章
|
26天前
|
API
[组件封装]API式调用-封装一个审核意见的组件Comments
[组件封装]API式调用-封装一个审核意见的组件Comments
11 0
|
6月前
|
设计模式 开发框架 开发者
组件封装使用?
组件封装使用?
antd组件库封装40-组件库封装1
antd组件库封装40-组件库封装1
79 0
antd组件库封装40-组件库封装1
antd组件库封装42-样式解决方案分析
antd组件库封装42-样式解决方案分析
88 0
antd组件库封装42-样式解决方案分析
|
API
api和封装,让Modal组件使用更加便捷
api和封装,让Modal组件使用更加便捷
131 0
|
存储 C++
C/C++ Qt 基础通用组件的应用
QT 是一个跨平台C++图形界面开发库,利用QT可以快速开发跨平台窗体应用程序,在QT中我们可以通过拖拽的方式将不同组件放到指定的位置,实现图形化开发极大的方便了开发效率。
314 0
C/C++ Qt 基础通用组件的应用
|
缓存 前端开发 数据库
【Django学习笔记 - 18】:drf请求响应简介、基类(APIView、GenericAPIView)、mixin扩展类与三级视图、视图集与路由2
【Django学习笔记 - 18】:drf请求响应简介、基类(APIView、GenericAPIView)、mixin扩展类与三级视图、视图集与路由
128 0
【Django学习笔记 - 18】:drf请求响应简介、基类(APIView、GenericAPIView)、mixin扩展类与三级视图、视图集与路由2
|
XML JSON 前端开发
【Django学习笔记 - 18】:drf请求响应简介、基类(APIView、GenericAPIView)、mixin扩展类与三级视图、视图集与路由
【Django学习笔记 - 18】:drf请求响应简介、基类(APIView、GenericAPIView)、mixin扩展类与三级视图、视图集与路由
171 0
【Django学习笔记 - 18】:drf请求响应简介、基类(APIView、GenericAPIView)、mixin扩展类与三级视图、视图集与路由