BetterScroll2.0 移动端滚动组件- React/vue

385 阅读4分钟

日常开发中,触摸滑动成为移动应用程序中不可或缺的交互方式。为了提供流畅、自然和高效的滑动体验,许多开发者选择使用开源库来简化开发过程。其中,BetterScroll 因其强大的功能和灵活的定制能力而备受关注。本文将深入探讨 BetterScroll 的工作原理、功能特点以及如何将其应用于实际项目中。

实现效果如下:

一.介绍

BetterScroll2.0 的官网地址是:better-scroll.github.io/docs/zh-CN/

它不依赖于任何框架,你可以在vue和react项目里面都可以使用。

image.png

二.原理

滚动原理:当子元素宽度或者高度大于父元素,并且父元素设置overflow:auto的时候,页面就会出现横向或者纵向的滚动条。如果我们把滚动条隐藏了,然后在子元素支持拖动事件,是不是就能达到想要的效果。

三.使用

BetterScroll有好几个版本,新版本下载是:

npm install @better-scroll/core --save
yarn add @better-scroll/core

使用:

import BScroll from '@better-scroll/core'


let wrapper = document.getElementById("wrapper") 
let bs = new BScroll(wrapper, {
//配置项
})

在react里面,我们将let wrapper = document.getElementById("wrapper") 利用 ref替换。

具体的配置项你可以查看地址: better-scroll.github.io/docs/zh-CN/…

需要什么配置什么就好了。

import BScroll from 'better-scroll'

const scroll = new BScroll(scrollContaninerRef.current, {
  scrollX: direction === "horizental",
  scrollY: direction === "vertical",
  probeType: 3,
  click: click,
  bounce: {
    top: bounceTop,
    bottom: bounceBottom
  }
});

四.封装-react

我们封装的Scroll组件

import React, { forwardRef, useState, useEffect, useRef, useImperativeHandle } from "react";
// import BScroll from "better-scroll";
import BScroll from '@better-scroll/core';
import './index.scss';

const Scroll = forwardRef((props, ref) => {
  const { direction = 'vertical', click = true, refresh = true, bounceTop = true, bounceBottom = true } = props;
  const { pullUp, pullDown, onScroll } = props;
  const [bScroll, setBScroll] = useState();
  const scrollContaninerRef = useRef();

  useEffect(() => {
    const scroll = new BScroll(scrollContaninerRef.current, {
      scrollX: direction === "horizental",
      scrollY: direction === "vertical",
      probeType: 3,
      click: click,
      bounce: {
        top: bounceTop,
        bottom: bounceBottom
      }
    });

    setBScroll(scroll);
    return () => {
      setBScroll(null);
    };
  }, []);

  useEffect(() => {
    if (!bScroll || !onScroll) return;
    bScroll.on('scroll', (scroll) => {
      onScroll(scroll);
    });
    return () => {
      bScroll.off('scroll');
    };
  }, [onScroll, bScroll]);

  useEffect(() => {
    if (!bScroll || !pullUp) return;
    bScroll.on('scrollEnd', () => {
      // 判断是否滑动到了底部
      if (bScroll.y <= bScroll.maxScrollY + 100) {
        pullUp();
      }
    });
    return () => {
      bScroll.off('scrollEnd');
    };
  }, [pullUp, bScroll]);

  useEffect(() => {
    if (!bScroll || !pullDown) return;
    bScroll.on('touchEnd', (pos) => {
      // 判断用户的下拉动作
      if (pos.y > 50) {
        pullDown();
      }
    });
    return () => {
      bScroll.off('touchEnd');
    };
  }, [pullDown, bScroll]);


  useEffect(() => {
    if (refresh && bScroll) {
      bScroll.refresh();
    }
  });

  useImperativeHandle(ref, () => ({
    refresh() {
      if (bScroll) {
        bScroll.refresh();
        bScroll.scrollTo(0, 0);
      }
    },
    getBScroll() {
      if (bScroll) {
        return bScroll;
      }
    }
  }));


  return (
    <div className="scroll-container" ref={scrollContaninerRef}>
      {props.children}
    </div>
  );
});

export default Scroll;

使用

import React, { useRef, useEffect, memo } from 'react';
import Scroll from '@/components/Scroll';
import './index.scss';

function Horizen(props) {
  const { list, oldVal, title } = props;
  const { handleClick } = props;

  const Category = useRef(null);

  // 加入初始化内容宽度的逻辑
  useEffect(() => {
    let categoryDOM = Category.current;
    let tagElems = categoryDOM.querySelectorAll("span");
    let totalWidth = 0;
    Array.from(tagElems).forEach(ele => {
      totalWidth += ele.offsetWidth;
    });
    categoryDOM.style.width = `${totalWidth}px`;
  }, []);

  return (
    <Scroll direction={"horizental"}>
      <div ref={Category}>
        <div className='song-list'>
          <span>{title}</span>
          {
            list.map((item) => {
              return (
                <span
                  key={item.key}
                  className={`list-item ${oldVal === item.key ? 'selected' : ''}`}
                  onClick={() => handleClick(item.key)}>
                  {item.name}
                </span>
              );
            })
          }
        </div>
      </div>
    </Scroll>
  );
}

export default memo(Horizen);

五.封装vue

<template>
  <div class="scroll-container">
    <div class="scroll-wrapper" ref="scroll">
      <div class="scroll-content">
        <div 
          class="scroll-item" 
          v-for="(item, index) in 100" 
          :key="index" 
          @click="clickHandler(item)"
        >
          第{{ item }}个
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue';
import BScroll from '@better-scroll/core'

const scroll = ref()
const bs = ref()

const init = () => {
  bs.value = new BScroll(scroll.value, {
    // 1. probeType 为 0,在任何时候都不派发 scroll 事件,
    // 2. probeType 为 1,仅仅当手指按在滚动区域上,每隔 momentumLimitTime 毫秒派发一次 scroll 事件,
    // 3. probeType 为 2,仅仅当手指按在滚动区域上,一直派发 scroll 事件,
    // 4. probeType 为 3,任何时候都派发 scroll 事件,包括调用 scrollTo 或者触发 momentum 滚动动画
    probeType: 3,
    click: true
  })
  bs.value.on('scrollStart', () => {
    console.log('scrollStart-')
  })
  bs.value.on('scroll', ({ y }: { y: number }) => {
    console.log('scrolling-', y)
  })
  bs.value.on('scrollEnd', (pos: { x: number, y: number }) => {
    // {x: 0, y: -792},滚动结束时容器相对x轴y轴坐标
    console.log(pos)
  })
}
const clickHandler = (item: number) => {
  window.alert(item)
}

onMounted(() => {
  init()
})

onBeforeUnmount(() => {
  bs.value.destroy()
})
</script>

<style lang="scss">
.scroll-container {
  .scroll-wrapper {
    height: 600px;
    position: relative;
    overflow: hidden;

    .scroll-item {
      height: 50px;
      line-height: 50px;
      font-size: 24px;
      font-weight: bold;
      border-bottom: 1px solid #eee;
      text-align: center;

      &:nth-child(2n) {
        background-color: #f3f5f7;
      }

      &:nth-child(2n+1) {
        background-color: #42b983
      }
    }
  }
}
</style>

.scroll-container {
  .scroll-wrapper {
    position: relative;
    width: 90%;
    margin: 80px auto;
    white-space: nowrap;
    border: 3px solid #42b983;
    border-radius: 5px;
    overflow: hidden;

    .scroll-content {
      display: inline-block;

      .scroll-item {
        height: 50px;
        line-height: 50px;
        font-size: 24px;
        display: inline-block;
        text-align: center;
        padding: 0 10px;
      }
    }
  }
}

六.常见的问题汇总

1、为什么 BetterScroll 初始化不能滚动?

BetterScroll 滚动原理是 content 元素的高度/宽度超过 wrapper 元素的高度/宽度。而且,如果你的 content 元素含有不固定尺寸的图片,你必须在图片加载完之后,调用 refresh() 方法来确保高度计算正确。还存在一种情况是页面存在表单元素,弹出键盘之后,将页面的视口高度压缩,导致 bs 不能正常工作,依然是调用 refresh() 方法。

2、为什么 BetterScroll 区域的点击事件无法被触发?

BetterScroll 默认会阻止浏览器的原生 click 事件。如果你想要 click 事件生效,BetterScroll 会派发一个 click 事件,并且 event 参数的 _constructed 为 true。配置项如下:

import BScroll from '@better-scroll/core'

let bs = new BScroll('./div', {
  click: true
})

3、为什么我的 BetterScroll 监听 scroll 钩子,监听器不执行?

BetterScroll 通过 probeType 配置项来决定是否派发 scroll 钩子,因为这是有一些性能损耗的。probeType 为 2 的时候会实时的派发事件,probeType 为 3 的时候会在 momentum 动量动画的时候派发事件。建议设置为 3。

import BScroll from '@better-scroll/core'

let bs = new BScroll('./div', {
  probeType: 3
})

4、slide 用了横向滚动,发现在 slide 区域纵向滚动无效?

如果想要保留浏览器的原生纵向滚动,需要如下配置项:

import BScroll from '@better-scroll/core'

let bs = new BScroll('./div', {
  eventPassthrough: 'vertical'
})