本文为稀土技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

阅览本文,你将

  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云:

克隆一个元素节点会仿制它所有的特点以及特点值,当然也就包括了特点上绑定的事情 (比方onclick=”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-scrollposition 不应该被限定,能够运用 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,是要排除掉那些空格文本(它们的 nodeTypeNode.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-topbox-bottom 这两个列表一起存在时,它们的子元素为奇数,以及子元素为偶数,所需求覆写款式的思路会出现偏差。

五、DEMO & 文档

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

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

文档地址: windstorm-ui SeamlessScroll组件

源码地址:github.com/zhangshichu…