1. 问题的由来
前段时间要做一个分页器, 大概是下面这个样子:
1 2 3 ... 7 [8] 9 ... 12 13 14
只要有一点相关经验的人都知道, 这个看似简单的东西, 实现起来其实是很麻烦的. 原因在于, 你面对的"总页数和当前页的关系", 不一定是上面这种"理想情况". 比如:
一共只有 3 页:
1 2 3
一共有 4 页:
1 2 3 4
一共有 6 页:
1 2 3 4 5 6
一共有 7 页(当前页是第 6 页):
1 2 3 ... 5 [6] 7
一共有 7 页(当前页是第 4 页):
1 2 3 [4] 5 6 7
一共有 14 页, 当前页是 13 页(最开始的例子, 当前页是第 8 页):
1 2 3 ... 12 [13] 14
上面几个简单的例子, 足以说明问题, 这玩意儿真的很麻烦, 如果你用传统的方法去做.
回到理想情况的例子:
1 2 3 ... 7 [8] 9 ... 12 13 14
这个分页器的显示规则, 大概可以描述成 " 显示前 3 页, 显示后 3 页, 以及当前页的前后 1 页 " .
或者, 可以扩展为 " 显示前 L 页, 显示后 R 页, 以及当前页的前后 M 页 " .
2. 自然的, 麻烦的思路
1 2 3 ... 7 [8] 9 ... 12 13 14
要实现这个东西, 可能大部分人(包括我)一来的想法, 就是通过总页数与当前页的条件, 去计算 左 / 中 / 右 三部分的结果. 整个计算过程穷举所有可能的情况, 即处理所有的状态.
这样做不能说不行, 因为市面上的好多分页器大概也是这样干的吧. 理论上可行, 但是实践中, 这样做的话成本太大了. 通过前面的例子也看到的, 面对的各种状态情况, 可以说非常非常复杂. 即使做出来了, 后续的维护, 需求变更的响应, 这些仍然需要继续以很大的成本去投入它, 稍有不慎, 出了问题要想定位到问题都困难吧, 在一堆涉及各种状态的代码中.
3. 换个角度入手
1 2 3 ... 7 [8] 9 ... 12 13 14 L PRE M POST R
理想情况下的结果, 其实也是一个"完备"的结果. 我们从这个形式着手, 细致地分析一下我们面对的到底是什么东西.
可以看到, 完整的结果, 一共是由 5 部分构成的, L , PRE , M , POST , R .
但是在一些"特殊"情况下, 这 5 部分不是全都出现的, 比如, 一共有 7 页(当前页是第 6 页):
1 2 3 ... 5 [6] 7
这时, 结果中只有 L , PRE, R(M) 这几部分(最后即可以看成是 M , 也可以看成是 R ).
传统的思路, 我们会去想, 通过计算, 算出 L , PRE , M , POST , R 这 5 部分的结果(包括它们是否应该存在), 但是, 这样很难.
要得到结果, 我们还有另外一种选择, 那就是首先, 我们忽略状态, 根据具体的规则, 先计算出结果.
规则是我们之前提到的: " 显示前 3 页, 显示后 3 页, 以及当前页的前后 1 页 " . 假设现在共有 S 页, 当前页是第 P 页, 那么结果就是:
L = [1, 2, 3] PRE = True M = [P - 1, P, P + 1] POST = True R = [S - 2 , S - 1 , S]
那么我们就可以得到:
1 2 3 ... P-1 [P] P+1 ... S-2 S-1 S
毫无疑问, 上面的计算与结果都没有任何问题, 那我们拿一个"特殊"的实例来看看结果是什么, 就用前面的"一共有 S = 7
页, 当前是第 P = 6
页":
1 2 3 ... 5 [6] 7 ... 5 [6] 7
剩下的事情, 就是去修辞这个看起来不太对的结果了, 这部分其实很容易(但是容易遗漏).
- 当
M[0] - L[-1] >= 2
时, PRE 才显示出来. - 当
R[0] - L[-1] >= 2
时, POST 才显示出来. M
中只显示比L[-1]
大的.R
中只显示比M[-1]
大的.L
,M
,R
中, 都只显示不比S
大的.L
,M
,R
中, 都只显示比0
大的(这点在计算L
,M
,R
时就随便做了).
上面简单的几条规则, 就可以把不太对的结果, 修饰成(括号中的内容不会显示):
1 2 3 ... 5 [6] 7 (...) (5 [6] 7)
来看看更多的例子:
S=1, P=1
:
[1] 2 3 ... 0 [1] 2 ... -1 0 1 --> [1] (2 3) (...) (0 [1] 2) (...) (-1 0 1)
S=4, P=3
:
1 2 [3] ... 2 [3] 4 .. 2 [3] 4 --> 1 2 [3] (...) (2 [3]) 4 (...) (2 [3] 4)
"修饰" 这部分, 实践中在一些框架的模板部分, 都可以直接做了.
4. 为何会有如此大的区别
换个角度去考虑之后, 一个原本麻烦的问题, 被轻松解决了. 之后我一直在想, 怎么会有如此大的区别呢?
我们实现的东西是一样的, 这就是说, 不管是传统的方法, 还是"变计算为应对"的方法, 它们的"描述能力"应该是一样的. 传统方法是在主动穷举所有的可能, 而后者则是在被动应对具体的可能.
当你在穷举所有可能的状态时, 不同类型的状态之间, 还有着相互的影响, 于是, 相互的作用大大放大了你面对的信息量. 这也许就是为什么我们会觉得非常麻烦的原因所在.
而当变成被动的应对这种方式时, 不同类型的状态之间, 相互的影响就不存在了(因为我们仅仅是信息的接收者, 没有创造信息, 也没有影响信息), 我们面对的只是独立的种种情况. 这种时候信息量就大大减少, 只需要简单地就每一种情况作出响应即可.
我知道上面的话很抽象, 其实我本身现在也无法对这种奇妙的区别作出一种具体的描述. 但要类比的话, 这个小小的分页器的实现, 以及大到系统架构的设计, 我觉得都跟绘画是一样的 -- 从大到小, 从抽象到具体, 从整体到局部 -- 如果一开始就从细节着力, 除非你已经胸有成竹, 否则连基本的正确比例都把握不了吧.
5. AngularJS的代码例子
最后, 附上使用 AngularJS 实现的上面说的分页器的简单代码:
scope.L_COUNT = 2; scope.M_OFFSET = 1; scope.R_COUNT = 2; //计算左,中,右分页 var get_group = function(count, per_page, page){ scope.sum_page = Math.ceil(count / per_page); if(page > scope.sum_page || page <= 0){page = 1; scope.page = page} scope.left = []; scope.middle = []; scope.right = []; for(var i = 0, l = scope.L_COUNT; i < l; i++){ scope.left.push(i+1); } for(var i = 0, l = scope.M_OFFSET; i < l; i++){ scope.middle.push(page-i-1); } scope.middle.push(page); for(var i = 0, l = scope.M_OFFSET; i < l; i++){ scope.middle.push(page+i+1); } for(var i = 0, l = scope.R_COUNT; i < l; i++){ scope.right.unshift(scope.sum_page-i); } }
模板部分:
<div> <div ng-show="sum_page > 1"> <div ng-repeat="o in left" ng-show="o <= sum_page" ng-click="goto(o)" ng-class="{true: 'u-current'}[o == page]">{{ o }}</div> <div ng-show="middle[0] > left[L_COUNT - 1] + 1">···</div> <div ng-repeat="o in middle" ng-show="o > left[L_COUNT - 1] && o <= sum_page" ng-click="goto(o)" ng-class="{true: 'u-current'}[o == page]">{{ o }}</div> <div ng-show="right[0] > middle[M_OFFSET * 2] + 1">···</div> <div ng-repeat="o in right" ng-show="o > middle[M_OFFSET * 2] && o <= sum_page" ng-click="goto(o)" ng-class="{true: 'u-current'}[o == page]">{{ o }}</div> </div> </div>