瀑布流列表的下拉刷新和上拉加载实现(下)

@laizimo 2017-09-01 15:50:19发表于 laizimo/zimo-article Javascript实践类已归档

前言

上篇我在文章中分析了如何去实现瀑布流的布局,以及怎么使用模版去实现数据的插入到html之中。这一篇,主要讲一下三部分的实现,1.数据mock、2.下拉加载、3.上拉刷新。本次demo的地址在此,喜欢的可以给一个Star

正文

首先,我们来分析一下数据的mock部分。由于想要实现与具备后台一样的效果,我们必须使用node去实现一个mock的后台,将数据放在json中,然后使用fs模块读取里面的内容,然后根据url进行数据的检索。

一般来说,下拉加载的数据都是具备分页特性的。主要可分为data(数据段)、page(当前页码)、next(告知前端接下来是否还有数据内容)。

之后,我们通过express框架对整个后端的数据进行一下模拟。我的demo目录下,有已经构造好的json文件,一共是两份,一份是更新前的数据,一份是更新后的数据。具体实现的代码:

const Express = require('express');
const app = new Express();
const path = require('path');
const fs = require('fs');

//data part

app.get('/:id', function(req, res){
    const page = req.params.id;
    res.setHeader("Access-Control-Allow-Origin", "*");
    // console.log(page);
    fs.readFile(path.resolve(__dirname, 'mock.json'), function(err, data){
        if(err){
            throw err;
        }
        const static = JSON.parse(data).data;
        const arr = Array.from(static);
        const len = arr.length;
        const next = page - 0 + 1 <= len ? true : false;
        for(let value of arr){
            if(value.page == page){
                res.json({data: value.data, next: next});
            }
        }
    });
});

app.get('/update/:id', function(req, res){
    const page = req.params.id;
    res.setHeader('Access-Control-Allow-Origin', '*');
    fs.readFile(path.resolve(__dirname, 'update.json'), function(err, data){
        if(err){
            throw err;
        }
        const static = JSON.parse(data).data;
        const arr = Array.from(static);
        const next = page - 0 + 1 <= arr.length ? true : false;
        let result = [];
        for(let value of arr){
            if(value.page <= page){
                result = result.concat(value.data);
            }
        }
        res.json({data: result, next: next});
    });
});

app.listen(3000);

这部分主要是读取json文件中的数据,然后将它一个固定的json格式返回给前端。源码地址

之后,我么开始来实现h5的下拉加载部分。

如果是我自己写的话,我会将下拉加载分成三个步骤进行实现:

  1. 使用ajax+promise的形式,对数据进行获取,然后去使用插入模版的函数,对数据进行插入
  2. 在window上监听scroll的事件,这里需要做一些性能的优化,比如说函数防抖,或函数截流的工作。因为scroll事件在android是触发的频率比较的高,会导致事件不断的发生,但是,往往scroll事件没有必要执行的这么频繁。频繁的触发事件会导致浏览器的性能下降。所以,截流和防抖都是会减缓触发的频率。
  3. 使用函数去判断页面是否滚动到页面的底部,通过视口的高度+滚动的高度 = 页面高度的方式来进行判断。

但是,这样子的实现方式往往不是最佳的方式。最佳的方式,是类似与iscroll和better-scroll插件的方式。原理:在最外层设置一个wrapper(包),将包的大小绝对定位和内容隐藏,然后给内部的内容添加transform的动画属性,使得它可以进行上下的滚动。这样的好处是,可以实现回弹的效果。

原理图:

原理图

better-scroll在iscroll上面做了一些修缮,尤其是在触摸滚动方面吧。所以可以使用better-scroll来实现下拉加载和上拉刷新。

第一步:固定外层包的大小,使用绝对定位的方式。然后在内容的上下都添加loading图。示例:

<div id="wrapper">
        <div>
            <div class="pull-refresh" id="pull-refresh">
                <div class="arrow" id="arrow">
                        <img src="https://raw.githubusercontent.com/AlloyTeam/AlloyTouch/master/refresh/pull_refresh/asset/arrow.png" alt="arrow"><br>
                </div>
            </div>
            <div id="toploading">
                <img src="./images/200.gif" alt="loading">
            </div>
            <ul class="wrapper">
            </ul>
            <div class="bottom" id="bottom">
                <span class="line"></span>
                <span class="tip">我是底线</span>
                <span class="line"></span>
            </div>
            <div class="loading" id="loading">
                <img src="./images/200.gif" alt="loading">
            </div>
        </div>
    </div>

之后就是一些样式的设置。

第二步:初始化scroll的对象,可以通过BScroll去进行构造。然后在每次载入内容的时候都需要去刷新一下scroll,保证它对内部内容的识别是正确的。否则,你可能只能滚动一部分。(由于是ajax载入数据的,所以在刷新的时候需要一定的延时,才能保证内容的正确)

//init scroll part

_initScroll: function(){
        const _self = this;
        const wrapper = document.getElementById('wrapper')
        const options = {
                probeType: 1,
                click: true,
                scrollbar: false,
                bounduceTime: 2000
        };
       _self.scroll = new BScroll(wrapper, options);
},


//ajax part

        fetch: function(url){
            return new Promise((resolve, reject) => {
                $.ajax({
                    url: url,
                    type: 'GET',
                    dataType: 'json',
                    data: {},
                    success: function(data){
                        resolve(data);
                    },
                    error: function(){
                        reject('request timeout');
                    }
                });
            });
        },
        load: function(fn){
            const _self = this;
            const url = _self.config.base_url + _self.data.page;
            _self.operations.bShow();
            _self.scroll.refresh();
            return _self.fetch(url).then(data => {
                setTimeout(() => {
                    _self.operations.bHide();
                    _self.data.next = data.next;
                    if(!data.next){
                        _self.bottomLine();
                    }
                    _self.data.page++;
                    fn.call(_self, data.data);
                    _self.data.status = 'ready';
                }, 500);
                setTimeout(() => {
                    _self.scroll.refresh();
                }, 600);
            }, err => {
                console.log(err);
            }).catch(e => console.log(e));
        },
        update: function(fn){
            const _self = this;
            const url = _self.config.base_url + 'update/' + (_self.data.page - 1);
            _self.operations.tShow();
            _self.operations.aHide();
            return _self.fetch(url).then(data => {
                setTimeout(() => {
                    _self.operations.tHide();
                    _self.operations.aShow();
                    _self.data.next = data.next;
                    fn.call(_self, data.data);
                }, 500);
                setTimeout(() => {
                    _self.scroll.refresh();
                    _self.data.status = 'ready';
                }, 600);
            }, er => {
                console.log(err);
            }).catch(e => console.log(e));
        },

这部分主要分为三个函数fetch、load、update。它们的作用分别是fetch主要是一个发起ajax请求的函数,以promise的形式,将内容返回回来,便于后面的数据操作。load主要作用就是获取下拉加载的数据,将它加载到html中去,update是上拉刷新后的数据,将它重新替换html中列表部分的内容。这里有一个status去控制加载的状态,防止重复发送ajax请求。

第三步:就是给scroll去添加事件。主要是两个事件touchEnd和scroll事件。这里的scroll事件是当滚动列表时发生整体的滚动时才会触发的。因为我们的html结构中,wrapper内的内容除了ul列表之外头部和尾部都还具备loading的标签。

        bind: function(){
            const _self = this;
            _self.scroll.on('touchEnd', function(position){
                if(position.y < _self.scroll.maxScrollY - 20){
                    _self.more();
                }else if(position.y > 80){
                    _self.update(_self.initHtml);
                }
            });
            _self.scroll.on('scroll', function(position){
                if(position.y > 80){
                    _self.operations.up();
                }else{
                    _self.operations.down();
                }
            });
        },

然后通过后端传过来的next可以帮助我们去判断是否还有后续内容,position.y对应的就是transform动画滚动时的值。然后,去判断是否到达底部是,我们还需要的是scroll中maxScrollY这个值。其中的20只是一个阈值。

总结

这样其实一个下拉刷新和上拉加载的过程就算是完成了。后续还可以改进的地方有是图片的懒加载和js模版引擎的使用。