通用feeds组件封装技巧

05731b7c5bef0fb02c18d8682c66802e.gif

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

需求分析

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

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

f0ad7f931ce120d4270e4add4676fd56.png

pic1:模块排布方式

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

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

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

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

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

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

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

团队介绍

我们是阿里巴巴大淘宝技术部汽车技术团队,是一支集研发、数据、算法一体的部门,利用互联网+数字化垂直整合汽车行业,打造消费者线上看车、买车、养车的极致体验。汽车业务是未来电商板块的蓝海,团队用持续的技术创新驱动业务稳步前行。
在这里,你会接触到新零售核心技术,交易、供应链、结算、阵地运营等。
在这里,团队氛围融洽,业务发展迅猛,技术挑战多多,让你有商业思考,又有技术深度。
阿里汽车高速发展的路上,期待您的加入!

✿  拓展阅读

31bc8920b1e69a5b1003e4d2086c9edc.jpeg

3953e5676a0c71a37f20fefed92f7b21.jpeg

作者|张新逵(伏加)

编辑|橙子君

f1a741f7d83082242e2a0b5cef1e2973.png

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值