react 移动端下拉刷新

7,476 阅读2分钟

前提

网上有很多针对vue封装的移动端UI组件库,但react的移动端UI组件库貌似只有Google的
material UI和阿里的 ant design mobile阿里的下拉刷新又不符合项目的风格,只能
自己实现了
采用better-scroll+react实现

效果

为什么要采用better-scroll

better-scroll 是一款重点解决移动端(已支持 PC)各种滚动场景需求的插件。它的核心
是借鉴的 iscroll 的实现,它的 API 设计基本兼容 iscroll,在 iscroll 的基础上又
扩展了一些 feature 以及做了一些性能优化。 
另外 better-scroll 中已经提供了下拉刷新 上拉加载更多的方法,我要做的也是在其方法
内完善我要的效果

下拉刷新

pullDownRefresh选项,用来配置下拉刷新功能。当设置为 true 或者是一个 Object 的时候,开启下拉刷新,可以配置顶部下拉的距离(threshold)来决定刷新时机,以及回弹停留的距离(stop)

options.pullDownRefresh = {
  threshold: 50, // 当下拉到超过顶部 50px 时,触发 pullingDown 事件
  stop: 20 // 刷新数据的过程中,回弹停留在距离顶部还有 20px 的位置
}

this.scroll = new BScroll(this.$refs.wrapper, options)

监听 pullingDown 事件,刷新数据。并在刷新数据完成之后,调用 finishPullDown() 方法,回弹到顶部边界

this.scroll.on('pullingDown', () => {
  // 刷新数据的过程中,回弹停留在距离顶部还有20px的位置
  RefreshData()
    .then((newData) => {
      this.data = newData
      // 在刷新数据完成之后,调用 finishPullDown 方法,回弹到顶部
      this.scroll.finishPullDown()
  })
})

上拉加载更多

pullUpLoad选项,用来配置上拉加载功能。当设置为 true 或者是一个 Object 的时候,可以开启上拉加载,可以配置离底部距离阈值(threshold)来决定开始加载的时机

options.pullUpLoad = {
  threshold: -20 // 在上拉到超过底部 20px 时,触发 pullingUp 事件
}

this.scroll = new BScroll(this.$refs.wrapper, options)

监听 pullingUp 事件,加载新数据。

this.scroll.on('pullingUp', () => {
  loadData()
    .then((newData) => {
      this.data.push(newData)
  })
})

直接上代码

import React, { Component } from "react";
import PropTypes from "prop-types";
import BScroll from "better-scroll";
import icon_arrow from "@assets/images/other/arr.png";
//样式
import "./betterScroll.less";

let defaultPullDownRefresh = {
  threshold: 100,
  stop: 50,
  stopTime: 600,
  txt: {
    success: "刷新成功"
  }
};

let defaultPullUpLoad = {
  threshold: 0,
  txt: {
    more: "加载更多",
    nomore: "我是有底线的"
  }
};

class Scroll extends Component {
  static defaultProps = {
    probeType: 3,
    click: false, // https://ustbhuangyi.github.io/better-scroll/doc/options.html#tap
    startY: 0,
    scrollY: true,
    scrollX: false,
    freeScroll: true,
    scrollbar: true,
    pullDownRefresh: false,
    pullUpLoad: false,
    bounce: true,
    preventDefaultException: {
      className: /(^|\s)originEvent(\s|$)/,
      tagName: /^(INPUT|TEXTAREA|BUTTON|SELECT|TABLE)$/
    },
    eventPassthrough: "",
    isPullUpTipHide: true,
    disabled: false,
    stopPropagation: true
  };

  static propTypes = {
    children: PropTypes.any,
    probeType: PropTypes.number,
    startY: PropTypes.number,
    click: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
    scrollY: PropTypes.bool,
    scrollX: PropTypes.bool,
    freeScroll: PropTypes.bool,
    scrollbar: PropTypes.bool,
    pullDownRefresh: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]),
    pullUpLoad: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]),
    pullUpLoadMoreData: PropTypes.func,
    canRenderPullUpTip: PropTypes.bool,
    doPullDownFresh: PropTypes.func,
    doScroll: PropTypes.func,
    doScrollStart: PropTypes.func,
    doScrollEnd: PropTypes.func,

    preventDefaultException: PropTypes.object,
    eventPassthrough: PropTypes.string,
    isPullUpTipHide: PropTypes.bool,
    bounce: PropTypes.bool,
    disabled: PropTypes.bool,
    stopPropagation: PropTypes.bool
  };

  constructor(props, context) {
    super(props, context);

    this.scroll = null; // scroll 实例

    this.isRebounding = false;
    this.pulling = false;

    this.pullDownInitTop = -50;
    // this.pullDownInitTop = 0;

    this.state = {
      isPullUpLoad: false,
      beforePullDown: true,
      pulling: false,
      pullDownStyle: {
        top: `${this.pullDownInitTop}px`
      },
      bubbleY: 0
    };
  }

  createScrollId() {
    return Math.random()
      .toString(36)
      .substr(3, 10);
  }

  componentDidMount() {
    this.initScroll();
  }

  componentDidUpdate(prevProps) {
    if (this.props.children !== prevProps.children) {
      if (!this.state.pulling) {
        this.scroll.refresh();
      }
      if (prevProps.disabled !== this.props.disabled) {
        this.props.disabled ? this.scroll.disable() : this.scroll.enable();
      }
    }
  }

  componentWillUnmount() {
    this.scroll.stop();
    this.scroll.destroy();
    this.scroll = null;
    clearTimeout(this.TimerA);
    clearTimeout(this.TimerB);
  }

  initScroll() {
    let {
      probeType,
      click,
      startY,
      scrollY,
      scrollX,
      freeScroll,
      scrollbar,
      pullDownRefresh,
      pullUpLoad,
      preventDefaultException,
      eventPassthrough,
      bounce,
      stopPropagation
    } = this.props;
    let _pullDownRefresh =
      typeof pullDownRefresh === "object"
        ? {
          ...defaultPullDownRefresh,
          ...pullDownRefresh
        }
        : pullDownRefresh
        ? defaultPullDownRefresh
        : false;

    let _pullUpLoad =
      typeof pullUpLoad === "object"
        ? {
          ...defaultPullUpLoad,
          ...pullUpLoad
        }
        : pullUpLoad
        ? defaultPullUpLoad
        : false;

    this.options = {
      probeType,
      click,
      startY,
      scrollY,
      freeScroll,
      scrollX,
      scrollbar,
      pullDownRefresh: _pullDownRefresh,
      pullUpLoad: _pullUpLoad,
      preventDefaultException,
      eventPassthrough,
      bounce: bounce,
      stopPropagation: stopPropagation
    };
    let wrapper = this.refs.$dom;
    this.scroll = new BScroll(wrapper, this.options);
    this.initEvents();
  }

  initEvents() {
    if (this.options.pullUpLoad) {
      this._initPullUpLoad();
    }
    if (this.options.pullDownRefresh) {
      this._initPullDownRefresh();
    }
    if (this.props.doScrollStart) {
      this.scroll.on("scrollStart", pos => {
        this.props.doScrollStart(pos);
      });
    }
    if (this.props.doScroll) {
      this.scroll.on("scroll", pos => {
        this.props.doScroll(pos);
      });
    }
    if (this.props.doScrollEnd) {
      this.scroll.on("scrollEnd", pos => {
        this.props.doScrollEnd(pos);
      });
    }
    if (this.props.disabled) {
      this.scroll.disable();
    }
  }

  getScrollObj = () => {
    return this.scroll;
  };

  _initPullDownRefresh() {

    this.scroll.on("pullingDown", () => {
      //松开手时
      this.setState({
        beforePullDown: false,
        pulling: true
      });
      this.props.doPullDownFresh().then(() => {
        //刷新方法调用成功
        if (!this.scroll) {
          return;
        }
        this.setState({
          pulling: false
        });
        this._reboundPullDown().then(() => {
          this._afterPullDown();
        });
      });
    });

    this.scroll.on("scroll", pos => {
      const { beforePullDown } = this.state;
      if (pos.y < 0) {
        return;
      }

      if (beforePullDown) {
        this.setState({
          bubbleY: Math.max(0, pos.y + this.pullDownInitTop),
          pullDownStyle: {
            top: `${Math.min(pos.y + this.pullDownInitTop, 10)}px`
          }
        });
      } else {
        this.setState({
          bubbleY: 0
        });
      }

      if (this.isRebounding) {
        this.setState({
          pullDownStyle: {
            top: `${10 - (defaultPullDownRefresh.stop - pos.y)}px`
          }
        });
      }
    });
  }

  _reboundPullDown = () => {
    let { stopTime = 4000 } = this.options.pullDownRefresh;
    return new Promise(resolve => {
      this.TimerA = setTimeout(() => {
        this.isRebounding = true;
        this.scroll.finishPullDown();
        resolve();
      }, stopTime);
    });
  };

  _afterPullDown() {
    this.TimerB = setTimeout(() => {
      this.setState({
        beforePullDown: true,
        pullDownStyle: {
          top: `${this.pullDownInitTop}px`
        }
      });
      this.isRebounding = false;
      this.scroll.refresh();
    }, this.scroll.options.bounceTime);
  }

  _initPullUpLoad = () => {
    this.scroll.on("pullingUp", () => {
      this.setState({
        isPullUpLoad: true
      });

      this.props.pullUpLoadMoreData().then(() => {
        if (!this.scroll) {
          return;
        }
        this.setState({
          isPullUpLoad: false
        });
        this.scroll.finishPullUp();
        this.scroll.refresh();
      });
    });
  };

  renderPullUpLoad() {
    let { pullUpLoad, isPullUpTipHide } = this.props;

    if (pullUpLoad && isPullUpTipHide) {
      return (
        <div className="b-pullup-wrapper">
          <div className="after-trigger" style={{ lineHeight: ".32rem" }}>
            <span style={{ color: "#999999", fontSize: ".28rem" }}>{""}</span>
          </div>
        </div>
      );
    }

    if (pullUpLoad && this.state.isPullUpLoad) {
      return (
        <div className="b-pullup-wrapper">
          <div className="after-trigger" style={{ lineHeight: ".32rem" }}>
            <i className="loading-icon"></i>
            <span style={{ color: "#999999", fontSize: "13px" }}>
              {typeof pullUpLoad === "object" ? pullUpLoad.txt.more : "加载中..."}
            </span>
          </div>
        </div>
      );
    }
    if (pullUpLoad && !this.state.isPullUpLoad) {
      return (
        <div className="b-pullup-wrapper">
          <div className="before-trigger">
            <span style={{ color: "#999999", fontSize: "13px" }}>
              {typeof pullUpLoad === "object" ? pullUpLoad.txt.nomore : "加载完成"}
            </span>
          </div>
        </div>
      );
    }
  }

  renderPullUpDown() {
    let { pullDownRefresh } = this.props;
    let { beforePullDown, pulling, pullDownStyle, bubbleY } = this.state;
    let cls = "arrow";
    if (pullDownRefresh && beforePullDown) {
      if (bubbleY > 50) {
        cls += " up";
      }
      return (
        <div className="b-pulldown-wrapper" style={pullDownStyle}>
          <div className={"after-trigger"}>
            <img src={icon_arrow} className={cls}/>
            <span>
              {bubbleY > 50 ? "松开立即刷新" : "下拉刷新"}
           </span>
          </div>
        </div>
      );
    }

    if (pullDownRefresh && !beforePullDown && pulling) {
      return (
        <div className="b-pulldown-wrapper" style={pullDownStyle}>
          <div className={"after-trigger"}>
           <span>
              加载中...
           </span>
          </div>
        </div>
      );
    }

    if (pullDownRefresh && !beforePullDown && !pulling) {
      return (
        <div className="b-pulldown-wrapper" style={pullDownStyle}>
          <div className={"after-trigger"}>
            <div>
              <span>
                {typeof this.options.pullDownRefresh === "object"
                  ? this.options.pullDownRefresh.txt.success
                  : "刷新完成"}
              </span>
            </div>
          </div>
        </div>
      );
    }
  }

  render() {
    return (
      <div className="b-wrapper" ref="$dom">
        <div className="b-scroll-content">
          {this.props.children}
          {this.renderPullUpLoad()}
        </div>
        {this.renderPullUpDown()}
      </div>
    );
  }
}

export default Scroll;

总结:

那位大佬有更好的实现方式 欢迎支出