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

特性

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

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

安装

1
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,你需要对它的数据进行分页显示,我们按如下方式对他下手即可。

1
2
3
4
5
6
7
8
// 原有存放数据的 collection
Players = new Mongo.Collection("players");

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

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

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

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

个性化

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
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 参数的源代码来判断问题出在了哪里,请看代码和注释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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 项目减少一些麻烦,一起努力共勉。