C端(react)丝滑swiper封装

203 阅读4分钟

介绍

在C端应用中,轮播组件作为一个高性能的组件,经常在页面中出现。如果引入第三方的组件库,可能会遇到以下问题:组件功能单一且难以扩展,打包后的代码体积过大,以及对外部组件的依赖性过强。因此,封装一个C端的Swiper组件来解决这些问题。 npm地址

方案

如何创建高性能 CSS 动画

实现方式:本质上,这是一系列的DOM元素,通过调整这些DOM的偏移量,使得内部的卡片看起来仿佛在滚动。

方式position:left方式前端控制位移量方式并使用transform:translateX() 滑动全部交给浏览器并使用 transform:translateX() 推荐
使用监听页面拖拽,引起页面的重绘,明显卡顿,舍弃监听页面的拖拽事件,使DOM容器随之移动。当用户放手后,通过 requestAnimationFrame API 实现DOM的缓慢位移(每次位移的偏移量是固定的)。优点:1. 较丝滑实现 2. 能够精准获取页面偏移,缺点:1. 页面偏移量不好控制 2. 不同尺寸的设备偏移结束的时间会不同 3. 回弹效果比较生硬监听页面的拖拽事件,使DOM容器随之移动。当用户松手后,为DOM元素设置 transition-duration: 300ms 属性,让浏览器处理过渡动画。然后,监听 transitionend 事件,以便在滚动完成后执行相应的操作。优点:1. css 动画,依赖原生属性,丝滑滚动 2. 不同设备上,不同拖拽偏移量都能丝滑的滚动完成。

应用

基本的复杂使用

缩放效果

实现

基本的实现

通过监听 touch 事件,让 dom 配合移动,在放手的时候,dom 移动完成整个卡片

wrapEle.current?.setAttribute(
  'style',
  `transform: translateX(${-left.current.toFixed(
  2
  )}px); transition-duration: ${transitionDuration}ms;`
);

然后监听dom完成事件

const transitionend = (e: TransitionEvent) => {
    if (e.propertyName === 'transform' && wrapEle.current) {
      const style = wrapEle.current.getAttribute('style') || '';
      wrapEle.current.setAttribute(
        'style',
        style?.replace(/transition-duration:\s(\d+)ms/, (obj, res) => {
          return obj.replace(res, '0');
        })
      );
      endTouch?.();

      _update((old) => old + 1);

      done();
    }
  };

回弹效果

判断 dom 移动的距离是否大于设置的伐值

 const touchend = (e: Event) => {
    removeEventListener(e.target as Element);
    if (!moving.current) {
      return;
    }

    if (wrapEle.current) {
      const width = getChildWidth() || 0;
      // 是否回弹
      if (Math.abs(hasMove.current) > width * bounce) {
        setTranslate();
        return;
      } else {
        if (hasMove.current) {
          setTranslate(true);
        } else {
          moving.current = false;
        }
      }
    }
  };

无限滚动

可以一直重复向左、向右滚动。在保证滚动对象的数量大于1的前提下,把对象的第一位复制放在最后,同时把复制前的最后一位复制放在第一位,并在滚动结束后更改dom的位置

 useEffect(() => {
    if (list.length) {
      if (infinite && list.length > 1) {
        const end = list[list.length - 1];
        const first = list[0];
        const oldList = [...list];
        oldList.unshift(end);
        oldList.push(first);
        setSwiperList(oldList);
      } else {
        setSwiperList(list);
      }
    }
  }, [list.length, infinite]);

移动结束:调整滚动结束后卡片的位置

if (infinite) {
   if (activeIndex.current === -1) {
       setActiveIndex(list.length - 1);
       left.current = width * list.length;
   } else if (activeIndex.current === list.length) {
       setActiveIndex(0);
       left.current = width;
   }
   setStyle();
} 

缩放

通过传入 dom 的缩放比例,让滚动卡片跟随滚动动态变化,需要把当前展示的dom初始化设置100%比例,其他元素为缩放比例。

if (scale) {
    let sameIndex = -1;
    child.forEach((ele, index) => {
      if (infinite) {
        if (index === activeIndex.current + 1) {
          sameIndex = Number(child[index].getAttribute('data-same'));
        } else {
          setTransform(ele as HTMLElement, scale);
        }
      } else {
        if (index !== activeIndex.current) {
          setTransform(ele as HTMLElement, scale);
        }
      }
    });
    if (sameIndex > -1) {
      setTransform(child[sameIndex] as HTMLElement, 1);
    }
  }

无限滚动缩放情况:需要把复制的元素同时缩放、扩大,这里会给复制 dom 添加 data-same 属性来控制

自动轮播

定时让 dom 自己去轮播

移动到特定卡片,上一个、下一个

 const swiperTo = (index: number) => {
    clearTimer();
    if (list.length === 1 || moving.current) {
      return;
    }
    if (index >= -1 && index !== activeIndex.current) {
      moving.current = true;
      startTouch?.(activeIndex.current);
      let targetIndex = index;
      if (!infinite && targetIndex >= list.length) {
        targetIndex = list.length - 1;
      }

      const moveLen = activeIndex.current - targetIndex;
      nextActiveIndex.current = targetIndex;
      hasMove.current = moveLen > 0 ? -1 : 1;
      // dom 运动
      setTranslate();
    }
  };

封装时候遇到的问题

  • 把偏移量的数值付给react中的style属性,会导致页面滚动卡顿,原因:react不是实时更新的,解决:直接取dom元素,然后直接更改style属性

  • 判断是左右移动、上下移动

    • 偏移量 X 轴的变化大于 Y 轴的变化,就是水平移动,然后取消滚动的默认行为就能保持
  • 移动的时候不要让整个 dom 跟着移动

  • 卡片滚动的时候,在切换卡片的时候卡片会有明显的闪光的效果,解决:启动硬件加速渲染

  • 处理缩放的场景下,transform: scale(90%) 在12pm上失效,解决:统一改成 transform: scale(0.9)

todo

  • 虚拟组件
  • 动态渲染,组件将要展示的时候才渲染内容
  • 组件支持vue
  • 支持滚动子组件为不同的宽度、上下滚动