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


相关文章
|
4月前
|
Web App开发 人工智能 API
基于虚拟试妆(VTO)AI API 的 Web 集成深度实践
美妆电商迈入3.0时代,Perfect Corp.推出YouCam for Web虚拟试妆AI API,以轻量化Web方案实现高精度AR试妆。支持浏览器端实时渲染、多材质还原、跨平台兼容,并内置隐私合规机制,助力品牌提升转化、降低退货,重塑线上美妆体验。
327 5
|
前端开发 JavaScript 定位技术
React 地图组件 Mapbox 入门指南
Mapbox 是一个强大的地图平台,提供丰富的地图数据和工具,支持多种开发语言和框架。本文介绍如何在 React 项目中使用 Mapbox,涵盖基础概念、安装配置、基本用法、常见问题及解决方法、高级用法等内容,并通过代码示例详细说明,帮助开发者提升地图应用的开发效率和用户体验。
857 2
|
缓存 前端开发 UED
React Suspense 懒加载详解
【10月更文挑战第18天】React Suspense 是 React 16.6 引入的新特性,主要用于处理异步数据获取和组件懒加载。本文从 Suspense 的基本概念出发,介绍了其在代码分割和数据获取中的应用,通过具体代码示例展示了如何使用 `React.lazy` 和 `Suspense` 实现组件的懒加载,并探讨了实践中常见的问题及解决方法,帮助开发者提升应用性能和用户体验。
754 3
|
存储 缓存 JavaScript
闭包有什么应用场景呢
【10月更文挑战第12天】闭包有什么应用场景呢
|
缓存 JSON 前端开发
你知道304吗?图解强缓存和协商缓存
该文章深入解析了HTTP协议中的缓存机制,重点讲述了强缓存和协商缓存的工作原理,并解释了HTTP状态码304的意义及其对前端和后端的影响。
|
机器学习/深度学习 存储 前端开发
实战揭秘:如何借助TensorFlow.js的强大力量,轻松将高效能的机器学习模型无缝集成到Web浏览器中,从而打造智能化的前端应用并优化用户体验
【8月更文挑战第31天】将机器学习模型集成到Web应用中,可让用户在浏览器内体验智能化功能。TensorFlow.js作为在客户端浏览器中运行的库,提供了强大支持。本文通过问答形式详细介绍如何使用TensorFlow.js将机器学习模型带入Web浏览器,并通过具体示例代码展示最佳实践。首先,需在HTML文件中引入TensorFlow.js库;接着,可通过加载预训练模型如MobileNet实现图像分类;然后,编写代码处理图像识别并显示结果;此外,还介绍了如何训练自定义模型及优化模型性能的方法,包括模型量化、剪枝和压缩等。
1097 1
|
缓存 前端开发 编译器
有了 React.createElement 为什么还需要 JSX runtime,作用是什么?
之前的一篇 基于 Webpack 从 0 到 1 启动一个 React 项目 文章中有介绍的是如何从 0 到 1 配置 React 项目中的 JSX 转换,在查阅文档时有介绍到从本质,JSX 只是为
|
Java Maven
springboot如何进行简单的测试
springboot如何进行简单的测试
855 0