前言

最近在写的项目中有上拉加载更多的需求。抽空就来写一篇文章。
上拉加载更多,下拉刷新,在原生 App 上经常都用到,既符合用户的使用习惯,也有很多成熟的库可以直接拿来使用。那么在WebApp中怎么实现呢?今天就我们就探讨一下。

一、思路

上拉刷新的可以通过判断某个点到窗口顶部的距离的值与浏览器窗口高度的值的大小来实现。假如说标识点在 WebApp 的底部,我们可以通过 js API Element.getBoundingClientRect().top 拿到它到屏幕顶部的距离,假如说是 200px 。同样的,可以利用 window.screen.heligh 拿到屏幕的高度,假如是 190px 。页面上滑,标志点到屏幕顶部的距离就会变小(这是废话…)。当距顶部距离小于190px时,调用加载数据的函数,就实现了上拉加载更多的功能。下拉刷新的逻辑与此大同小异,就不赘述了。另外,为了在上下滑网页时,需要对网页滚动进行监听,以保证我们对比的函数能够被触发。

二、实现

接下来就以 react.js 为例来写一个DEOM 实现一下。

准备工作
  1. create-react-app demo 创建一个名为 demo 的项目。
  2. cd demo && yarn start 启动程序。
  3. 打开 demo/src/App.js 修改代码如下。
import React, { Component } from 'react';
import './App.css';

class App extends Component {
  constructor(props) {
    super(props);
    this.state = ({
      data: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'],
      isLoadingMore: false
    });
  }

  render() {
    return (
      <div>
        <div className="App">
        {this.state.data.map((item, index) => (
            <li key={index} className="li-item">{item}</li>
          ))}
        </div>
        <div className="loadMore" ref="wrapper" onClick={this.loadMoreDataFn.bind(this, this)}>加载更多</div>
      </div>
    );
  }
}

export default App;
  1. 修改 demo/src/App.css如下
.App {
  width: 100%;
}

.li-item {
  height: 100px;
  display: flex;
  justify-content: center;
  align-items: center;
}

.loadMore {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 50px;
  border: 1px; 
  border-style: solid;
  border-color: red;
}
  1. 浏览器打开 http://localhost:3000/,单击右键,检查,toggle device bar,让 chrome 模拟在手机上的效果。我们应该看到如下的页面。
    屏幕快照 2017-07-18 下午9.55.09.png
三、实现逻辑

在 react 中,操作dom时一般用 ref。因此给“加载更多”所在 div 设置ref='wrapper',在 componentDidMount 方法中来实现业务逻辑。此处不明白为什么在此周期操作的同学应该再去看一遍 react 的LifeCycle。

在 componentDidMount 中,先定义一个变量用来保存dom。
const wparrer = this.refs.wrapper; 定义一个变量,用来降低屏幕滚动监听的频率。let timeCount;

接下来定义一个函数,内容如下:

function callback() {
    const top = wrapper.getBoundingClientRect().top;
    const windowHeight = window.screen.height;

    if (top && top < windowHeight) {
        // 当 wrapper 已经被滚动到页面可视范围之内触发
        loadMoreDataFn(that);
    }
}

top保存的是“加载更多”那个div距屏幕顶部的距离。Element.getBoundingClinentRect()的使用见MDN - Element.getBoundingClinentRect
windowHeight保存的就是窗口的高度。通过对比top与windowHeight的高度,来调用加载更多的函数。

接下来我们再给屏幕滚动设置一个监听事件:

window.addEventListener('scroll', function () {
        if (this.state.isLoadingMore) {
            return ;
        }

        if (timeCount) {
            clearTimeout(timeCount);
        }

        timeCount = setTimeout(callback, 50);
    }.bind(this), false);

通过上述代码,就可以对屏幕滚动进行监听,timeCount变量是用来做延时的,检测到屏幕滚动后,延时50ms进行下一次检测。优化体验,也保证了性能。

loadMoreDataFn 在本文中,为了实现上拉加载更多的效果,此处写一个操作数组的逻辑,将一些数据放到this.state.data之后。当然,实际的开发中,也是这样做,不过合并的数据是 fetch 从 api 获取的。

  loadMoreDataFn(that) {
    that.setState({
      data: that.state.data.concat(['E', 'c', 'h', 'o'])
    })
  }

四、完整的代码


demo/src/App.jsx:

import React, { Component } from 'react';
import './App.css';

class App extends Component {
  constructor(props) {
    super(props);
    this.state = ({
      data: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'],
      isLoadingMore: false
    });
  }

  render() {
    return (
      <div>
        <div className="App">
        {this.state.data.map((item, index) => (
            <li key={index} className="li-item">{item}</li>
          ))}
        </div>
        <div className="loadMore" ref="wrapper" onClick={this.loadMoreDataFn.bind(this, this)}>加载更多</div>
      </div>
    );
  }

  componentDidMount() {
    const wrapper = this.refs.wrapper;
    const loadMoreDataFn = this.loadMoreDataFn;
    const that = this; // 为解决不同context的问题
    let timeCount;


    function callback() {
            const top = wrapper.getBoundingClientRect().top;
            const windowHeight = window.screen.height;

            if (top && top < windowHeight) {
              // 当 wrapper 已经被滚动到页面可视范围之内触发
              loadMoreDataFn(that);
            }
    }

    window.addEventListener('scroll', function () {
            if (this.state.isLoadingMore) {
                return ;
            }

            if (timeCount) {
                clearTimeout(timeCount);
            }

            timeCount = setTimeout(callback, 50);
        }.bind(this), false);
  }

  loadMoreDataFn(that) {
    that.setState({
      data: that.state.data.concat(['E', 'c', 'h', 'o'])
    })
  }
}

export default App;

demo/src/App.css:

.App {
  width: 100%;
}

.li-item {
  height: 100px;
  display: flex;
  justify-content: center;
  align-items: center;
}

.loadMore {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 50px;
  border: 1px; 
  border-style: solid;
  border-color: red;
}