大屏经典组件:“无限滚动” 从分析到开发

news2024/11/15 18:01:09

📖阅读本文,你将

  1. 理解大屏 “无限滚动组件” 的开发思路
  2. 跟随作者,一步步完成一个高性能 “无限滚动组件” 的开发
  3. 收获一份该实现的粗糙源码。

一、无限滚动:事件/告警 的有力帮手

1.1 为什么需要滚动列表

大屏之所以 “炫酷” ,相比于 UI 同学出的效果图,它最大的优势就在于 它能动

哪怕平台可能没有接入 websocket,甚至数据就是静态写死的,客户依然希望数据能在屏幕上 “动起来”。

这会给人一种 “数据是实时的” 的错觉。

这种错觉,或者说故意营造出来的错觉,就是领导们 “讲故事” 的素材之一。

尤其是当业务里涉及到 “事件/告警/威胁/监控” 等元素时,涉及到的数据量很大 —— 几百或几千条,此时,会自己滚动的列表就成了非常适合场景的组件形式:

“ 我们相关部门单据的申请和审批情况也会实时推送到系统中,可以做到实时把控。 ”

——领导如此向上介绍。

虽然大家都明白,但是谁会整天没事盯着一个深色的大屏做监管呢?这么炫酷的大屏,电脑不卡吗?

正经人都是用 "白色底+蓝色按钮" 的后台管理系统进行业务操作的。

可是汇报的时候,小小的列表就是关于 “实时监管” 的一个有力佐证。

1.2 为什么还得是 “无限滚动”?

但是普通列表有一些非常明显的弊端:

  • 它 有尽头
  • 它的滚动 没有质感
  • 它的衔接动画有 不连贯感

不理解?那我们看张图:

你有没有发现,它存在以下问题?

  • 滚动是平缓的,没有节奏感。(相比于上面一次滚一行,然后停止若干时长后,进行下一次滚动)

  • 滚动到最后一行后,即使立刻滚动到顶部,依然会产生明显的 “不连贯感” 。

为了解决以上问题,于是有了一种更为优质的 视觉体验组件,它具备以下特性:

  • 它似乎 没有尽头
    (滚动时,第一条数据就贴合在最后一条数据的后面,依此类推)
  • 它的动画 连贯又流畅
  • 它的滚动 更有质感

它就是 无限滚动,一个常见又经典的大屏组件。

二、实现思路分析

2.1 需求分析

ok,明确了 “无限滚动” 的必要性,让我们看看,它应该具备哪些特性?

假设,你有一个长度为4的列表,长这样:

那么它应该具备以下特性:

  1. 每次花费 N 秒滚动一单元格长度 (从A的上侧滚动到B的上侧)
  2. 每次滚动结束后停留 M 秒,方便参观者查看数据。
  3. 当 D 完全出现在视窗中之后,紧接着出现的应该是 A,然后是B,以此类推。

一个最简单的无限滚动组件,最少应该具备以上三个特性。

接下来,就是头脑风暴的时间了:

无限滚动的列表,究竟应该如何实现?

2.2 思路A:修改元素排序

这是最直观的思路,我们只持有原列表本身,通过滚动到一定阶段,调整的顺序,来完成 “无限” 的效果。

但是很可惜,这个方案:

存在较大弊端

比如,当视窗大小只略小于列表大小时,就会出现这种情况:

即:A元素,既要出现在顶端,但同时也要出现在尾端。

这样一来,单纯排序就无法完全满足诉求了。

2.3 思路B:不仅排序,还复制元素

为了解决上面 思路A 存在的问题,我们可以考虑通过 Node.cloneNode() 方式拷贝一个元素,手动让页面上同时存在两个A元素,一头一尾,就能补全上面那个场景的问题了。

但是,很可惜,这一方法也存在问题:

MDN云:

克隆一个元素节点会拷贝它所有的属性以及属性值,当然也就包括了属性上绑定的事件 (比如οnclick="alert(1)"),但不会拷贝那些使用addEventListener()方法或者node.onclick = fn这种用 JavaScript 动态绑定的事件。

简单来说,事件丢了。
最核心你的一点在于,通过改变元素结构来实现无限滚动这种方式,和 ReactVue 等集成了虚拟 DOM 的框架搭配使用时,也会遇到各种各样的结构同步的问题,会急剧增加框架的复杂性。

那么,有没有更简单的方法呢?

2.4 方案C:双倍的快乐

众所周知:

动画是欺骗眼睛的艺术。

在帧与帧之间,画面其实是割裂的,人眼所能感知的最短时间大概是 30ms,也就是说,如果按 30ms 作为间隔改变画面的形态,人眼就会认为画面是 连续的

因此,很多你看到的效果,其实都是在 欺骗你的眼睛

比如,你用两个完全相同的列表,就可以实现肉眼意义上的 无限滚动

如上图。

思路其实是:

  1. 两个完全相同的列表垂直排列,从头开始向下滚动。
  2. 当第一个列表的下端达到视窗的上端时(此时它已经不可见了),立刻让第一个列表滚动到上端与视窗的上端重合。
  3. 重复第一步

之所以,这个思路可行,有两个关键点:

  • 第2步改变状态前后,组件的视窗内看到的内容是一样的。
  • 第2步改变状态时,因为第二步是在瞬间完成的,并没有滚动过程,因此用户不会感知到发生过状态改变。

因此,用户就能一直感觉到: “这个列表在向下无限地滚动”

相比于 “方案A” 和 “方案B”,此方案最大的优势就在于:

  • 它首先不需要改变元素的顺序
  • 它也不需要去通过 cloneNode 复制单个元素

借用 props.children (react) 或者 <slot></slot> * 2 (vue),你就能轻易获得两份具备事件绑定的元素,逻辑简单又粗暴,不用编写复杂的代码。

综上所述,就用最轻轻松松的一笔,毁掉你所有的问题,我都选C,我都选C!

三、核心编码实现

Talk is cheap,show me your money code。

3.1 准备生产工具

首先,因为本系列都基于 vue3,因此,有一个可运作的 vue@3.x 环境是必要的,至于是 webpack 或是 vite 并不重要。

甚至可以是一个 UI 库脚手架。(文末提供的 demo 会是这种形式的。)

{

  "dependencies": {
    "gsap": "latest", // 我最顺手的动画库,当然你也可以选tween.js或者纯手写。
    "@vueuse/core": "latest", // vuer 必备的hooks工具库
  }
}
复制代码

ok,需要依赖的外部包就这些,接下来让我们开始建造。

3.2 元素布局设计

让我们思考组件的元素布局,在我的规划中,它大概长这样:

在类名设计上,我们采用业内组件开发最常用的 BEM 规范 (参考链接),由外到内,分别是:

  • .seamless-scroll:组件最外层元素。

  • .seamless-scroll__wrapper:具备 position: relative 和 宽高100% 的元素,目的是充满父元素。

    之所以采用这种冗余的布局方式,是为了满足更多场景的使用,比如.seamless-scroll的 position 不应该被限定,可以使用 absolutefixedrelative 等各种奇奇怪怪的布局。而 .seamless-scroll__wrapper 可以保证自身永远是 relative 状态的。

  • .seamless-scroll__box: 高度不受限的控件,它会在 .seamless-scroll__wrapper 的怀抱中滚动。

  • .seamless-scroll__box-top 和 .seamless-scroll__box-bottom 就是那两份一模一样的列表的容器,它们的高度来自于列表项的撑起。

3.3 API 设计

由于本文主要以讲解为主,目标不是做一个 “可以应对各种场景的组件”,因此我们只解决单一场景,所以 API 的设计上追求极致的简单:

const props = defineProps({
   /**
    * 两次滑动之间的停顿时长
    */
  delay: {
    type: Number,
    default: 1
  },
  /**
   * 滑动单位距离需要的时间
   */
  duration: {
    type: Number,
    default: 2
  }
})
复制代码

以及,提供了一个默认插槽。

<slot></slot>
复制代码

在这个插槽中,使用者可以去放列表的元素,它们各有各的高度和样式,这不应该是我们 无限滚动应该接管的内容 去接管的内容, 所以通过插槽的形式暴露出去。

3.4 DOM 结构及关键 CSS

关于 DOM 结构,只需要按本文 3.23.3 两个小节设计的思路,对照以下这张图就可以轻松完成构建:

<template>
  <div class="seamless-scroll">
    <div ref="wrapperRef" class="seamless-scroll__wrapper">
      <div ref="boxRef" class="seamless-scroll__box">
        <div class="seamless-scroll__box-top" ref="topRef">
          <slot></slot>
        </div>
        <div class="seamless-scroll__box-bottom">
          <slot></slot>
        </div>
      </div>
    </div>
  </div>
</template>
<script setup>
  const wrapperRef = ref(null)
  const boxRef = ref(null)
  const topRef = ref(null)
</script>
<style lang="scss">
  .seamless-scroll {
    &__wrapper {
      width: 100%;
      height: 100%;
      position: relative;
      overflow: hidden; // 我们希望wrapper滚动,但不希望他露出丑陋的滚动条
    }
    &__box {
      &-top,
      &-bottom {
        overflow: hidden;
      }
    }
  }
</style>
复制代码

另外有个小 TIPS:

关于封装组件时,<style> 标签要不要使用 scoped,我的建议是 “不要用 scoped,要用 BEM CSS 命名规范”,这样的好处在于方便其他组建对其引用样式,进行样式覆盖时,不会陷入 CSS 权重竟态问题。(不得不说,vue scoped 相关机制,在这方面比 react css module 更友好一点点)

3.5 让列表滚动起来

如果你有过使用 tweenjs 或者 gsap 这类动画库,你就能够明白,它们所做的最终要的一件事,就叫做 补间

所谓 补间 的意思,就是:

你指定一个对象,从 状态A,耗费 固定时间,以 特定的方式 变化到 状态B。而之后该对象在每一帧的表现,就不再需要由你关注,相关的工具会自动计算出每帧对象的中间状态,并完成显示。

理解了这一点,我们就能很好地想到,列表的 平滑滚动,其实就是把上面漫画里的 top 改成 scrollTop 的过程。

MDN ScrollTop 相关文档在此

所以,我们让列表滚动的核心代码,如下:

import gsap from 'gsap'

onMounted(() => {
  const timeLine = gsap.timeline() // 为了后续更复杂的时间线安排,我们引入了 gsap 的 timeline 
  timeLine.to(wrapperRef.value, { scrollTop: 200, duration: props.duration }, `+=${props.delay}`)
})
复制代码

就可以初步达到如下效果:

3.6 让列表 有质感地滚动

所谓 有质感 的滚动,其实是指 一行一行 地滚动。

所以,每一次滚动之前,我们都需要获得列表的 元素们,但我们是通过插槽形式插入的列表,应该怎么在 vue3 里获得这些元素呢?

  const nodeList = topRef.value.childNodes
  const nodeArr = Array.from(nodeList.values()).filter(t => t.nodeType === Node.ELEMENT_NODE)
复制代码

之所以要经过一轮 filter,是要排除掉那些空格文本(它们的 nodeType 是 Node.TEXT_NODE

再通过维护一个 scrollingElIndex 变量作为下标,记录当前滚动元素的 index,就能准确获得:“这一次,我应该滚多远” 这一重要信息。代码如下:

let scrollingElIndex = 0;
const currentScrollingEl = nodeArr[scrollingElIndex];
scrollingElIndex = (scrollingElIndex + 1) % nodeArr.length; // 取完记得让 `scrollingElIndex` 下标+1,但只能在元素个数之内循环
复制代码

接下来,我们需要计算元素的高度,此时,我推荐使用 getBoundingClientRect 方法,它和 clientHeight 的最大区别在于:它包含border,这会大大降低我们计算每个子元素高度的复杂度。

代码如下:

  let rect = currentScrollingEl.getBoundingClientRect();
  const elHeight = rect.height
  const offsetTop = currentScrollingEl.offsetTop
  const scrollTarget = offsetTop + elHeight;
复制代码

上面代码片段里获取到的 scrollTarget 就是此次滚动我们需要滚动到的 scrollTop 的值。

使用这个思路,就可以很容易得到如下效果:

3.6 让列表无限滚动

为了让列表达成无限滚动,按照我们 2.4 方案C:双倍的快乐 这一节的思路分析,其实核心就在于:

当上半部分的列表滚动到最后一个元素后,需要立刻让其恢复到初始位置。

这里只需要判断元素下标是否为 0 即可,非常容易:

  if (scrollingElIndex === 0) {
    gsap.to(wrapperRef.value, {
      scrollTop: 0, duration: 0, onComplete: () => {
        genAnimates()// 先滚动到顶端再思考下一步动画
      }
    })
  }
复制代码

上面的动画看似流畅,但其中已经包含了一次 偷梁换柱。在这个过程中,列表实际上就已经具备了 无限滚动的能力

四、一些补充能力

  • 当列表高度过小时,应避免滚动,这时候就不应该通过 <slot></slot> 再复制一份元素了。

    代码略,可参考文末源码

  • 当鼠标移动到列表上之后,停止滚动,移出去后接着滚动,这对 gsap.timeline 来说小菜一碟。

    const onMouseOver = () => {
      timeLine.pause()
    }
    
    const onMouseOut = () => {
      timeLine.resume()
    }
    复制代码
  • 给元素一个 奇偶数 的状态类名

    之所以需要给这个,是为了后续进行样式覆写,完成 斑马线 等效果,因为当 box-top 和 box-bottom 这两个列表同时存在时,它们的子元素为奇数,以及子元素为偶数,所需要覆写样式的思路会出现偏差。

五、DEMO & 文档

为了写这一专栏,本菜鸡专门起了一个 基于 vitepress 的 vue3 微型组件库,用来放置相关代码,以及相关文档。(特此感谢:dewfall123/ruabick 提供的脚手架 ,看库名就知道是一个老 DOTA2 玩家了)

当你需要在项目中使用到类似效果时,除了使用对你而言几乎是完全 黑盒 的开源库之外,你还可以参考本文,自行造轮子,自行沉淀组件库,并收获一个可视化开发上的小经验。

文档地址: windstorm-ui SeamlessScroll组件

源码地址:github.com/zhangshichu…

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/12924.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

稳压二极管稳压电路如何设计

在一些电流不大的地方&#xff0c;一般毫安级别&#xff0c;有时候我们可以利用稳压二极管去设计一个我们需要的电压。 大家可以看下稳压二极管的伏安曲线 在反向电压下&#xff0c;尽管电流在很大的范围内变化&#xff0c;而稳压二极管两端的电压却基本上稳定在击穿电压附近&a…

[附源码]java毕业设计旅游产品销售管理

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

CISCN 2022 初赛 web 复现

[CISCN 2022 初赛]ezpop 可以看到版本&#xff0c;那么直接上网找链子打 www.zip 查看路由&#xff0c;是 Index/test&#xff0c;然后 post 传参 a <?php // 保证命名空间的一致 namespace think {// Model需要是抽象类abstract class Model {// 需要用到的关键字priv…

【vue】vuex中modules的基本用法

1&#xff0c;什么时候用modules 由于使用单一状态树&#xff0c;应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时&#xff0c;store 对象就有可能变得相当臃肿。 为了解决以上问题&#xff0c;Vuex 允许我们将 store 分割成模块&#xff08;module&#xff09…

NFT交易平台开发 创建NFT数字藏品平台

为什么需要 NFT 市场&#xff1f; NFT Marketplace 允许用户购买、出售、交易、查看或创建自己的 NFT&#xff0c;就像他们需要一个市场来购买物理或数字世界中的大多数产品一样。几乎每个人都可以进入 NFT 市场&#xff0c;但要做到这一点&#xff0c;用户必须满足以下要求&a…

第3关:节点状态检查、数据查看和更新

首先&#xff0c;需要启动服务器&#xff0c;并使用zkCli.sh连接服务器&#xff0c;进入客户端命令行界面&#xff08;如第一关所述&#xff09;。 节点状态包含以下信息&#xff1a; czxid: 节点创建时的时间戳。mzxid: 节点最新一次更新发生时的时间。ctime&#xff1a; 节…

[附源码]java毕业设计篮球装备商城系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

微服务中的服务发现是什么?

作者&#xff1a;罗泽轩 摘要&#xff1a;本文通过服务发现的相关背景和 APISIX 对于服务发现的应用与实践&#xff0c;来介绍微服务中的服务发现内容。 在互联网刚开始出现的年代&#xff0c;人们要想访问某个在线服务&#xff0c;需要输入一长串的 IP 地址。IP 地址虽然不长&…

PDF怎么编辑修改文字?

PDF怎么编辑修改文字&#xff1f;PDF大家都不陌生&#xff0c;我们经常会接触到各种类型的PDF文档&#xff0c;尽管大多数人对PDF的需求仅仅是阅读&#xff0c;但是也有很多人会需要去编辑和修改PDF文件&#xff0c;那你们知道如何编辑和修改PDF文件吗&#xff1f;金闪PDF编辑器…

提取图像直方图(图像处理)

继直方图规定化后的研究 由于直方图规定化是根据两张图片的累计直方图进行图像的处理。对于这个处理过程&#xff0c;我尝试了很图像进行替换色系&#xff0c;但是没有找到一个相对合适案例&#xff0c;来体现直方图规定化这个算法的精妙之处。在多次尝试中&#xff0c;我发现…

低代码核心:代码生成还是模型解释?

2020年第一届低代码研讨会上&#xff0c; Jordi Cabot发表了一篇文章&#xff08;或者说观点&#xff09;&#xff0c;对比了低代码和模型驱动开发的关系&#xff0c;认为低代码等于模型驱动开发。但实际上&#xff0c;不少低代码系统并不是使用模型驱动的&#xff0c;而是采用…

node的express模块

express的概述: express是一个提供web服务的框架&#xff08;内置http模块&#xff09;&#xff0c;他简化了http的相关内容&#xff0c;将对应的内容封装为了特定的方法 安装: npm i express -S 导入以及代码: //导入express 是一个函数 const expressrequire(express) //cre…

做机器人开发,你一定绕不开的模块!

Allspark 机载电脑 Allspark 是阿木实验室为广大AI智能硬件开发者打造的一款微型边缘计算机。在设计之初就定义了尺寸小巧、重量轻、算力强、可靠、扩展性高的特点。Allspark机身采用铝合金新材料外壳设计&#xff0c;内置静音散热风扇&#xff0c;尺寸94mm*59mm*37mm&#xff…

[附源码]java毕业设计警院学生学习交流系统

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

Linux进程常见通信方式

文章目录1、管道<1>匿名管道<2> 命名管道2、共享内存3、信号量为什么要进程要进行通信呢&#xff1f; 进程间可能存在特定的协同工作的场景&#xff0c;这个时候就需要一个进程把自己的数据交付给另一个进程&#xff0c;让其进行处理。 进程通信的本质 因为进程具有…

OAuth2.o的授权码模式为什么要用code获取token?

授权码模式&#xff08;Authorization Code&#xff09;是 OAuth 功能最齐全、流程最严谨&#xff0c;也是最常用的授权模式。 假设我们要用微信账号登录网易云音乐&#xff0c;需要以下五步&#xff1a; 访问网易云音乐客户端&#xff0c;客户端跳转到微信授权页面&#xff…

神经网络初体验

文章目录前言相关概念BP神经网络具体过程正馈反向传播总结前言 本博客仅做学习笔记&#xff0c;如有侵权&#xff0c;联系后即刻更改 科普&#xff1a; 参考博客:《老饼讲解神经网络》 相关概念 神经网络 模仿人的神经网络构建出来的数学模型 是人工智能的一个主力算法 神经…

STM32 BSRR BRR ODR 寄存器解析(F4系列已经去掉BRR寄存器了)

STM32 BSRR BRR ODR 寄存器解析&#xff08;F4系列已经去掉BRR寄存器了&#xff09;一、用法二、解释三、BSRR、BRR、 ODR 之间的关系G0x0系列GPIO寄存器 F4系列GPIO寄存器&#xff08;没有BRR寄存器了&#xff09; 一、用法 经常会看到类似如下的宏定义语句&#xff0c;用…

tomcat出现中文乱码原因和解决办法(简单快捷易懂)

一、遇到问题 双击打开tomcat中的bin目录下的startup.bat会出现乱码问题 或者cmd里面打开也是乱码的问题 二、出现这个问题的原因 这是因为windows下的默认编码是GBK编码&#xff0c;tomcat默认编码是UTF-8编码 解决思路&#xff1a;那就把tomacat的默认编码改为和windows下…

HTML5-框架-计算机应用2115-2022年11月17日13:57:13

目录 HTML栅格化布局框架 2、demo演示 栅格化理论&#xff1a; 栅格化系统&#xff1a; 网页栅格化&#xff1a; 重点掌握内容&#xff1a; 练习目标: HTML栅格化布局框架 1、将整个HTML浏览器的宽度设为单位1&#xff0c;那么为了操作栅格化方便&#xff0c;我们拆分…