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


相关文章
|
人工智能 自然语言处理 数据可视化
Cursor 为低代码加速,AI 生成应用新体验!
通过连接 Cursor,打破了传统低代码开发的局限,我们无需编写一行代码,甚至连拖拉拽这种操作都可以抛诸脑后。只需通过与 Cursor 进行自然语言对话,用清晰的文字描述自己的应用需求,就能轻松创建出一个完整的低代码应用。
1661 8
|
前端开发 JavaScript 定位技术
React 地图组件 Mapbox 入门指南
Mapbox 是一个强大的地图平台,提供丰富的地图数据和工具,支持多种开发语言和框架。本文介绍如何在 React 项目中使用 Mapbox,涵盖基础概念、安装配置、基本用法、常见问题及解决方法、高级用法等内容,并通过代码示例详细说明,帮助开发者提升地图应用的开发效率和用户体验。
811 2
|
缓存 前端开发 UED
React Suspense 懒加载详解
【10月更文挑战第18天】React Suspense 是 React 16.6 引入的新特性,主要用于处理异步数据获取和组件懒加载。本文从 Suspense 的基本概念出发,介绍了其在代码分割和数据获取中的应用,通过具体代码示例展示了如何使用 `React.lazy` 和 `Suspense` 实现组件的懒加载,并探讨了实践中常见的问题及解决方法,帮助开发者提升应用性能和用户体验。
728 3
|
网络协议 安全 物联网
网络安全涨知识:基础网络攻防之DDoS攻击
网络安全涨知识:基础网络攻防之DDoS攻击
1086 0
|
数据采集 JavaScript 前端开发
Python 爬虫实战:抓取和解析网页数据
【8月更文挑战第31天】本文将引导你通过Python编写一个简单的网络爬虫,从网页中抓取并解析数据。我们将使用requests库获取网页内容,然后利用BeautifulSoup进行解析。通过本教程,你不仅能够学习到如何自动化地从网站收集信息,还能理解数据处理的基本概念。无论你是编程新手还是希望扩展你的技术工具箱,这篇文章都将为你提供有价值的见解。
1313 1
|
域名解析 安全 网络安全
使用Python自动检测SSL证书是否过期
使用Python自动检测SSL证书是否过期
691 0
|
SQL 关系型数据库 MySQL
Mysql从入门到精通——Mysql知识点总结(基础篇)
Mysql从入门到精通——Mysql知识点总结(基础篇)
901 0
|
缓存 前端开发 编译器
有了 React.createElement 为什么还需要 JSX runtime,作用是什么?
之前的一篇 基于 Webpack 从 0 到 1 启动一个 React 项目 文章中有介绍的是如何从 0 到 1 配置 React 项目中的 JSX 转换,在查阅文档时有介绍到从本质,JSX 只是为
|
机器学习/深度学习 算法 PyTorch
探索Python中的强化学习:DQN
探索Python中的强化学习:DQN
511 0

热门文章

最新文章