Meteor 分页包 alethes:pages 详解

在做大部分真实应用的 web 项目过程中,都会有一样不可或缺的需求,那就是分页。Meteor 项目也不例外,同样会有这样的需求,本文给大家介绍的就是一个非常好用的分页包 alethes:pages。它可以实现简单的根据页数分页,也可以实现强大的滚动分页。内部还支持利用多个 collection 数据进行分页。下面我们就来详细的了解它。

注意:以下内容多是对官方包的一种简述和翻译,如果需要更详细的内容可以参考官方地址。

特性

官网上介绍了很多它的特性,由于我的英文不是很好,我只能翻译我们大家非常关注的点。

  • 仅 subscribe 当前页需要的数据,并不是一次性 sub 所有数据
  • 本地缓存,获取过的数据本地存储,避免返回时重新获取
  • 在加载当前页过程中,预取下一页的数据,确保下一页的时候无缝过度
  • 多个集合产生一个分页数据
  • 支持 bootstrap 2/3 的分页导航模版
  • 支持 iron-router
  • 页面无限滚动加载特效

安装

meteor add alethes:pages

官网

atomsphere – https://atmospherejs.com/alethes/pages/

github – https://github.com/alethes/meteor-pages/

Demo 演示

基本的分页 – http://pages.meteor.com/

表格 (快速渲染) – http://pages-table.meteor.com/

多个 collection 自动刷新 – http://pages-multi.meteor.com/

滚动加载效果 – http://pages3.meteor.com/

使用

要使用这个包的功能非常简单,首先用你要实现分页的 collection 生成一个 PlayersPages 分页对象。

这里假设之前已经存在一个 Players 的 collection,你需要对它的数据进行分页显示,我们按如下方式对他下手即可。

// 原有存放数据的 collection
Players = new Mongo.Collection("players");

// 根据已经有数据的 collection 生成一个 Meteor.Pagination 类型的对象。
this.PlayersPages = new Meteor.Pagination(Players, {
  // 指定需要分页所使用的模版
  templateName: "playersTemplate"
});

上面的代码可以看到,我们指定了分页所使用的模版名字叫 playersTemplate,此时,我们只需要在增加一个这样的模版即可。

<template name="playersTemplate">
    {{> pages}}
    {{> pagesNav}}  <!--分页导航按钮-->
</template>

新建的模版中再导入另外两个模版 pagespagesNav,这个两个模版是分页包 alethes:pages 给我们创建的,用来显示数据用。如此这样添加后,playersTemplate 模版就可以分页显示 Players collection 的数据了。

个性化

但具体每页显示多少数据、显示数据的样式如何定义、分页导航按钮能不能换成滚动屏幕自动加载瀑布流的方式?等等类似的问题,这个包都提供了解决方案。我们先来看一下它比较重要的几个参数,用一个我们已经使用到项目中的代码片段+注释的方式,来给大家演示这个包的各种参数(更多参数可以参考 github 上的介绍)。

this.ProductPages = new Meteor.Pagination(Products, {
    // 调试模式
    debug: true,
    // 认证函数,内部可以写一些过滤,让一些不想被用户看到的数据被过滤掉
    auth: function (skip, sub) {

        // 判断是否有用户登陆
        if (!sub.userId) {
            return false;
        }

        // 清空过滤器内容
        this.filters = {};

        // userSettings 是每个不同用户本地不同的分页属性,如果有多个访问者,每个访问者可以设定自己的分页属性
        let userSettings = this.userSettings[sub._session.id] || {}
            , uFields = userSettings.fields || this.fields
            , uSort = userSettings.sort || this.sort
            , uPerPage = userSettings.perPage || this.perPage
            , _filters = userSettings.filters || this.filters
            , _options = {fields: uFields, sort: uSort, limit: uPerPage, skip: skip};

        // 将不想给用户看到的数据过滤掉,这里你可以写自己的过滤代码
        let shop = ReactionCore.getCurrentShop(this);
        if (shop) {
            _.extend(_filters, {shopId: shop._id});
            // products are always visible to owners
            if (!(Roles.userIsInRole(sub.userId, ["owner", "admin", "createProduct"], shop._id))) {
                _.extend(_filters, {isVisible: true});
            }
        }

        // 返回新的分页属性
        return [_filters, _options];
    },
    // 允许每个不同用户设定的分页属性有那些选项
    availableSettings: {
        limit: true,
        sort: true,
        filters: true
    },
    // 分页最外部的 div class 名
    divWrapper: 'row',
    // 是否启用滚动分页(瀑布流)
    infinite: true,
    // 滚动条加载到什么位置时加载下一组分页数据,这个参数问题比较多,后面再介绍
    infiniteTrigger: .8,
    // 滚动加载模式下,后续页面最小的加载时间间隔
    infiniteRateLimit: 1,
    // 暂时不用
    infiniteStep: 1,
    // 分页时每页数据中所有子项目所用的分页模版,你可以设定各种你已经美化过的模版
    itemTemplate: "productGridItems",
    // 分页所用的模版
    templateName: "infiniteProducts",
    // 最多显示多少数据
    pageSizeLimit: 1000,
    // 第一页加载多少数据
    perPage: 4,
    // 最大 subscribe 的数据两
    maxSubscriptions: 500,
    // 预加载页数,dataMargin * perPage + perPage = 页面首次打开显示的数据量
    dataMargin: 1,
    // 对数据进行排序
    sort: {
        order: 1, title: 1
    }
});

以上时参考官方给出的无限滚动模式下所使用到的参数,其中 infiniteTrigger 参数我在使用过程中遇到了很多问题。第一个问题就是滚动条滚动到 0.8 的位置后,数据不会自动继续加载。修正了第一个问题后,随后出现的问题时滚动条并非到 0.8 的位置才加载数据,而是我滚动条只要一动,下一页的数据就自动加载出来了,这样明显不对。

我们通过分析处理 infiniteTrigger 参数的源代码来判断问题出在了哪里,请看代码和注释。

Pages.prototype.setInfiniteTrigger = function() {
  return $(window).scroll((_.throttle(function() {
    var l, oh, t;
    // 获取你设定的 infiniteTrigger 值
    t = this.infiniteTrigger;
    // 获取当前 body 的高度,应该是页面所有数据的高度之和
    oh = document.body.offsetHeight;
    // 判断你设定的 infiniteTrigger 值是否大于 1 做不同的操作
    if (t > 1) {
      l = oh - t;
    } else if (t > 0) {
      // l = body 的高度 * 你设定的 infiniteTrigger 的高度
      // 假设 body 已经有 2500 的高度了,2500 * 0.8 = 2000
      l = oh * t;
    } else {
      return;
    }
    // 如果当前可视的页面高度 + 滚动条的位置 >= 上面计算出来 l 的值
    if ((window.innerHeight + window.scrollY) >= l) {
      // 加载下一组数据
      if (this.lastPage < this.sess("totalPages")) {
        return this.sess("currentPage", this.lastPage + 1);
      }
    }
  }, this.infiniteRateLimit * 1000)).bind(this));
};

分析过代码后,我得出判断,第一个问题时由于我们页面中有一个很大的 div 当作 body 来用,滚动的时候实际时 div 的滚动条在滚动,而 body 的滚动条一直在 0 的位置,所以无论你看到的 div 的滚动条滚动到了哪里,下一组数据都不会继续加载。

在第一个问题解决完以后,再继续分析第二个问题,首先清楚两个概念。

document.body.offsetHeight – body 整个页面的高度,一般是页面中所有元素加起来的高度之和。
window.innerHeight – 可视的高度,当前浏览器显示了多少内容,这些内容的高度之和。

我分别在页面中打印了一下 window.innerHeight 的值和 document.body.offsetHeight 的值,赫然发现两个值时相等的,所以导致我滚动条刚刚开始滚动的时候,window.innerHeight + window.scrollY 一定大于 document.body.offsetHeight * infiniteTrigger 的值。知道原因了,如何解决呢?为什么 document.body.offsetHeight 的值与 window.innerHeight 的值一样大呢?不应该是页面所有元素的高度吗?在分析别人的代码对比后发现,原来我们的 body 被设定了一个 css 样式为 height: 100vh;,该属性的意思就是将 body 的高度设定为可视的高度,所以 body 的高度与 window.innerHeight 就没什么区别了,最终导致了如上问题,将这个 css 修改为 height: 100% 即可解决问题。

总结

这个包需要研究的地方还有很多,希望我介绍的内容能帮助大家在后期开发 Meteor 项目减少一些麻烦,一起努力共勉。

发表评论