列表组件抽象(4)-滚动列表及分页说明

这是我写的关于列表组件的第4篇博客。前面的相关文章有:javascript

1. 列表组件抽象(1)-概述css

2. 列表组件抽象(2)-listViewBase说明html

3. 列表组件抽象(3)-分页和排序管理说明java

本文介绍列表组件中我对滚动列表及滚动分页的实现思路。git

在pc端,经过滚动进行翻页的需求很是常见;移动端也是,只不过移动端因为scroll事件触发有延迟,必须等到屏幕中止滑动后才会触发,而不是在用户的手指离开屏幕就当即触发,因此移动端最好是不用scroll事件直接作滚动翻页,而是用iscroll这类插件提供更实时的scroll事件更好。github

不论是pc仍是移动端,滚动翻页列表的特色都是差很少的:chrome

1)基本上由如下几个部分组成:数据列表,顶部的加载中提示,底部的加载中提示,没有更多了,没有找到记录。正是按照这个思路,因此我把滚动列表的html结构设计成:浏览器

image

2)跟其它列表组件不一样的是,滚动列表在请求新的数据后,有2种方式来渲染新的数据。一种是跟其它列表组件同样,直接把原来的列表内容替换;另外一种是将新数据追加在原有的列表内容以后。第1种一般用于直接更改列表的查询条件时使用;第2种用于翻页查询或者刷新操做。缓存

3)在前面的几个部分中,有两个加载中的提示,都是用来提高用户体验的东西。顶部加载提示用于条件查询,底部加载提示用于翻页查询。从它们在html中的位置也能看出来。app

4)加载更多的按钮,一是防止滚动事件失效而准备的,二是有些场景可能会禁用掉滚动翻页,因此就要提供直接点击按钮的手工翻页。

5)没有更多了这个部分,在翻页查询后,根据数据结果判断没有更多的数据时显示。

6)没有找到记录的这个部分用于在列表首次查询时,若是数据为空时显示。

7)当经过滚动或者滑动操做,使得滚动列表隐藏于可视区域之下的部分不断往上滚动,并在达到某一个临界点的时候,触发翻页查询,将下一页的数据追加到数据列表后面进行显示。

针对以上的这些需求逻辑,考虑pc端和移动端的场景,我写了两个组件分别用于实现滚动列表。同时与这两个列表组件一块儿使用的还有另外两个分页组件,它们两两之间是配套使用的。

首先是用于实现pc端,可相对window或者某个DOM元素进行滚动分页的列表组件scrollListView以及它配套的分页组件scrollPageView组件,源码分别是:

https://github.com/liuyunzhuge/blog/blob/master/form/src/js/mod/listView/scrollListView.js

https://github.com/liuyunzhuge/blog/blob/master/form/src/js/mod/listView/scrollPageView.js

而后是用于移动端,结合iscroll一块儿使用的iscrollListView和iscrollPageView组件,源码分别是:

https://github.com/liuyunzhuge/blog/blob/master/form/src/js/mod/listView/iscrollListView.js

https://github.com/liuyunzhuge/blog/blob/master/form/src/js/mod/listView/iscrollPageView.js

针对以上组件有如下demo能够查看相关功能演示:

pc端相对window滚动分页demo:http://liuyunzhuge.github.io/blog/form/dist/html/listView_2.html

pc端相对某个DOM元素滚动分页demo:http://liuyunzhuge.github.io/blog/form/dist/html/listView_3.html

移动端滚动分页demo:http://liuyunzhuge.github.io/blog/form/dist/html/listView_4.html

后面的部分说明以上组件的要点。不过因为iscrollListView直接继承了scrollListView,实现很是简单;iscrollPageView的实现思路也跟scrollPageView差很少。因此后面只介绍scrollListView和scrollPageView的相关内容。

先来看scrollListView.js。

首先,代码结构仍是跟前几篇博客介绍的组件都差很少,因此这里再也不复述。defaults是这样定义的:

var DEFAULTS = $.extend({}, ListViewBase.DEFAULTS, {
    //列表容器的选择器
    dataListSelector: '.data_list',
    //顶部加载中的html
    loadingTopHtml: '<div class="loading_top">加载中...</div>',
    //没有结果的html
    noContentHtml: '<div class="no_content">没有找到相关记录:(</div>',
    //底部加载中的html
    loadingBottomHtml: '<div class="loading_bottom">加载中...</div>',
    //没有更多的html
    noMoreHtml: '<div class="no_more">没有更多了</div>',
    //加载更多的html
    loadMoreHtml: '<a href="javascript:;" class="btn_load_more">加载更多</a>'
});

主要是用来定义滚动列表的那几个组成部分。若是不想用默认值,那么在实例化组件的时候,传入想要设置的option就好了。

scrollListView继承了listViewBase,为了增长本身的初始化逻辑,因此用到了initMiddle这个模板方法,并在其中作了一些jq对象缓存,以及内部状态管理初始化的逻辑:

initMiddle: function () {
    var opts = this.options,
        $element = this.$element,
        $data_list = this.$data_list = $element.find(opts.dataListSelector),
        $load_more = this.$load_more = $(opts.loadMoreHtml).appendTo($element),
        $no_content = $(opts.noContentHtml).appendTo($element),
        $loading_top = $(opts.loadingTopHtml).prependTo($element),
        $loading_bottom = $(opts.loadingBottomHtml).appendTo($element),
        $no_more = $(opts.noMoreHtml).appendTo($element);

    $load_more.css('display', 'block');

    //状态管理:初始化完毕,顶部加载中,底部加载中,没有结果,没有更多,加载完毕
    var states = this.states = {
        init: function () {
            $data_list.show();
            $load_more.hide();
            $no_content.hide();
            $loading_top.hide();
            $loading_bottom.hide();
            $no_more.hide();
        },
        loading_top: function () {
            $data_list.show();
            $load_more.hide();
            $no_content.hide();
            $loading_top.show();
            $loading_bottom.hide();
            $no_more.hide();
        },
        loading_bottom: function () {
            $data_list.show();
            $load_more.hide();
            $no_content.hide();
            $loading_top.hide();
            $loading_bottom.show();
            $no_more.hide();
        },
        no_content: function () {
            $data_list.hide();
            $load_more.hide();
            $no_content.show();
            $loading_top.hide();
            $loading_bottom.hide();
            $no_more.hide();
        },
        loaded: function () {
            $data_list.show();
            $load_more.show();
            $no_content.hide();
            $loading_top.hide();
            $loading_bottom.hide();
            $no_more.hide();
        },
        no_more: function () {
            $data_list.show();
            $load_more.hide();
            $no_content.hide();
            $loading_top.hide();
            $loading_bottom.hide();
            $no_more.show();
        },
        'set': function (action) {
            this.curState = action;
            this[action]();
        },
        isNomore: function () {
            return this.curState == 'no_more';
        },
        isNoContent: function () {
            return this.curState == 'no_content';
        }
    };

    states.set('init');
},

以上代码中的那个states对象用来实现内部的状态管理,能够看做一个简单的状态机。采用这个作法的缘由,一是为了知足最前面介绍滚动列表组件特色时描述的那些需求逻辑,二是为了让这些UI控制逻辑看起来更清晰。有了它,我只要在什么时候的时机,改变下组件的状态,就能列表组件显示不一样的内容了。比较简单好理解。

而后经过createPageView来实现分页组件的初始化逻辑。这里就得使用scrollPageView来实例化了:

createPageView: function () {
    var pageView,
        opts = this.options;

    if (opts.pageView) {
        //初始化分页组件
        delete opts.pageView.onChange;
        pageView = new ScrollPageView($.extend(opts.pageView, {
            $loadMore: this.$load_more,
            $element: this.$element
        }));
    }
    return pageView;
},

而后把scrollPageView须要的几个dom对象以option的形式传给了它。

考虑到滚动列表组件的特殊性,我还用到了listViewBase的其它几个模板方法来实现滚动列表的需求。

首先是beforeQuery:

beforeQuery: function (clear) {
    //若是clear为true,则显示顶部的加载状态,表示正在进行新条件的列表查询
    //不然显示底部的加载状态,表示正在进行翻页查询
    this.states.set(clear ? 'loading_top' : 'loading_bottom');
},

这个方法会接受一个参数clear,为true则表示进行条件查询,不然表示进行翻页查询。这个方法的做用在于查询前显示加载提示。

而后是querySuccess:

querySuccess: function (html, args) {
    var pageView = this.pageView,
        rows = this.originalRows,
        method = args.clear ? 'html' : 'append';//根据查询类型,来决定要如何处理渲染新的数据

    //没有查到结果
    if (rows.length == 0 && pageView.pageIndex == 1) {
        this.states.set('no_content');
        return;
    }

    //没有更多
    if (rows.length < pageView.pageSize) {
        this.states.set('no_more');
        html.length && this.$data_list[method](html);
        return;
    }

    //加载完毕
    this.states.set('loaded');
    this.$data_list[method](html);
},

它用来实现请求成功的后的逻辑,最重要的固然是渲染数据。可是考虑到列表组件的需求,还得根据多方面的参数,判断该把列表设置为何样的状态。请求成功后的结果无非三种,没有查到数据,没有更多,加载成功。这三个状态,根据分页信息和记录数就能判断清楚,见源码里面if逻辑的写法。

而后是queryError:

queryError: function () {
    this.states.set('loaded');
},

这个主要是在请求失败的时候,还原列表的状态而已。

最后是afterQuery:

afterQuery: function () {
    if (this.states.isNoContent() || this.states.isNomore()) {
        this.pageView.disable();
    }
}

它在请求完成以后,判断若是是没有数据或者没有更多的状态的话,就禁用掉分页组件,省得用户操做不慎致使还会发出一些查不到数据的请求。

以上就是scrollListView实现的核心了,只有100多行。

再来看scrollPageView。

它的defaults定义以下:

var DEFAULTS = $.extend({}, PageViewBase.DEFAULTS, {
    //加载更多的按钮
    $loadMore: null,
    //滚动元素
    $element: null,
    //滚动区域的目标元素,若是没有传,默认就是window对象,用来注册scroll事件
    $target: null,
    //是否启用滚动翻页
    scrollPage: true,
    //滚动元素底边跟滚动可视区域底边的距离,它是滚动翻页的临界点
    offset: -100,
    //滚动事件的绑定时的延迟时间
    scrollBindDelay: 0,
    //滚动事件的节流间隔
    throttle: 100,
});

应该好理解。offset的做用后面会继续说明,scrollBindDelay是用来延迟滚动事件绑定的。为啥会搞这个,是由于chrome浏览器有个特性,若是在浏览网页的时候,刷新以后,滚动条会恢复到刷新前浏览的位置,而且它这个自动恢复也会触发滚动事件。那么当列表组件初始化完毕以后,颇有可能会发出两次查询请求,一次是由初始化调用发出的,一次是由自动恢复的滚动事件发出的。因此加上这个option,有利于控制列表初始化后的首次请求。$loadMore用于注册点击事件,添加手动翻页的逻辑。$element表示滚动列表相关的dom对象。$target表示滚动相对的目标对象,若是不传,就指向window对象。

scrollPageView内部提供了简单的节流函数来作滚动事件回调的节流控制:

//简单函数节流
function throttle(func, interval) {
    var last = Date.now();
    return function () {
        var now = Date.now();
        if ((now - last) > interval) {
            func.apply(this, arguments);
            last = now;
        }
    }
}

也提供了获取css属性在浏览器重绘以后的值的函数:

//用来获取css某个属性通过浏览器重绘以后的值
function getComputedValue(element, prop) {
    var computedStyle = window.getComputedStyle(element, null);
    if (!computedStyle) return null;
    if (computedStyle.getPropertyValue) {
        return computedStyle.getPropertyValue(prop);
    } else if (computedStyle.getAttribute) {
        return computedStyle.getAttribute(prop);
    } else if (computedStyle[prop]) {
        return computedStyle[prop];
    }
}

其它代码却是没啥好补充的,重点看下滚动事件相关的翻页控制逻辑,我就只贴了相关的匿名函数代码了:

function () {
    if (that.disabled) return;

    var targetHeight, bottom;

    //目标元素的clientHeight做为滚动区域的高度
    //bottom:滚动元素的底边到滚动区域顶边的距离

    if (!opts.$target) {
        targetHeight = document.documentElement.clientHeight;
        bottom = opts.$element[0].getBoundingClientRect().bottom;
    } else {
        targetHeight = opts.$target[0].clientHeight;

        var targetRect = opts.$target[0].getBoundingClientRect(),
            targetBorderTop = parseInt(getComputedValue(opts.$target[0], 'border-top-width')),
            elemRect = opts.$element[0].getBoundingClientRect();

        //若是target是其它的html元素,因为都是采用boundingClientRect来计算的,因此要减去目标元素顶部边框的宽度,这样才不会有偏差
        bottom = elemRect.bottom - targetRect.top - (isNaN(targetBorderTop) ? 0 : targetBorderTop);
    }

    //bottom+ opts.offset等于一条临界线
    //若是opts.offset小于0,那么这条临界线就位于滚动元素底边再往上opts.offsets的距离的位置
    //若是Opts.offset大于0,那么这条临界线就位于滚动元素底边再往下|opts.offsets|的绝对距离的位置
    //翻页触发的条件是:这条临界线恰好出如今滚动区域里面的时候
    if ((bottom + opts.offset) < targetHeight) {
        pageIndexChange(that.pageIndex + 1, that);
    }
}

以上的思路也比较简单,只要判断列表元素的底部跟目标对象的可视区域的底部达到临界距离便可,这个临界距离就是defaults中定义的offset值。以相对window滚动为例说明如何来作这个判断:

image

根据上图,能够得知翻页临界的判断条件就是上图中临界线到目标对象可视区域顶边的距离小于目标对象可视区域的高度。这个图虽然是以相对window的滚动状况来讲明问题的,可是相对DOM对象进行滚动的判断方式跟这个是如出一辙的,只要咱们可以获得DOM对象的可视区域高度以及临界线到DOM对象可视区域顶边的距离便可。个人代码中是利用getBoundingClientRect来求的,至关于仍是以浏览器的可视区域的顶边做为参考线,不过考虑到普通的DOM对象可能也有顶部边框的问题,在计算最后的临界线到DOM对象可视区域的顶边距离时,减去了DOM对象顶部变宽的宽度。只有这样得出的临界线距离才是相对于DOM对象可视区域顶边而言的。

以上就是本文的所有内容,介绍了我想要补充说明的关于滚动列表组件的部分。这一块内容,我以为没有特别广的适用性,毕竟各个产品对滚动翻页这种需求的逻辑可能也不尽相同,我这边提供的是我现有项目中的实现思路,可能只能对您有必定的参考价值。因此要是有不妥的,欢迎随时帮我指正出来。谢谢阅读:)

相关文章
相关标签/搜索