第九集: 从零开始实现( 分页器组件 )
本集定位:
分页器这个组件也算是个老朋友了, 还记得刚学js的时候, 写个分页器要300行代码,要是能穿越回去, 我得好好教教我自己设计模式.
随着现在手机地位的提升, 大部分人上网的时间都用在了手机上, pc端的确是少了很多很多, 而分页器这种类型的组件, 真的并不很适合手机, 已经不符合人类的操作体验了, 人们现在都是划划划或拉拉拉的动作驱动翻页, 分页器其实要配合鼠标才能有很好的感受, 人的手指点击并不精准, 而且点击的时候还会遮盖住视野, 反正本人更推荐pc端使用分页器来跳转, 移动端可不要这么玩, 多想点让用户更舒服的操作吧.
本次编写参考了饥人谷的视频, 同时也看了element的源码, 但最终还是按我自己的想法构建了这个组件.
- 增加了自己的一些理解, 实现方式与想法挺有趣的, 毕竟代码这种东西不是一成不变的, 更多的玩法才能使编程更有生命力,
- 去掉了比如说一个输入框, 可以跳转到固定页数, 这个功能我去年做了半年的后台管理系统, 一次也没用上.
- 没有去想传统组件一样, 用户传入总条数, 然后我来给分成对应的页数, 而是采用用户自己传入分成了多少页.
- 本次激活的页面会以v-model的形式与用户进行交互, 也就是说这个变量是双向的, 上面说去掉的两个功能就非常好实现了, 但本次不会做, 毕竟实践经验告诉我, 做了真没啥用
一. 基本结构
这个结构是参考的别人的源码... 还有挺好的, 虽然dom写的有点丑, 但是逻辑清晰, 易维护.
老样子
vue-cc-ui/src/components/Pagination/index.js
import Pagination from './main/pagination.vue';
Pagination.install = function(Vue) {
Vue.component(Pagination.name, Pagination);
};
export default Pagination;
躯壳
- 单独写了 第一位与最后一位, 默认就是这两位要一直展示给用户选择
- ...这种标志也是单独写了, 毕竟首尾都直接写了, 那他也可以这样操作
- 左右两边的两个按钮允许用户插入自己的代码
- 这个结构的好处就是把问题具体化了, 不用考虑其他的, 当前核心问题就是如何求出中间for循环的数据,也就是本集的重点了.
- 1
- ··
- {{item}}
- ··
- {{总页数}}
css 方面
@include b(pagination) {
cursor: pointer;
color: #606266; // 这个颜色很柔和的黑
align-items: center;
display: inline-flex;
justify-content: center;
.btn-prev { // 按钮去掉默认样式
border: none;
outline: none;
background-color: transparent;
&:hover { // 这个nomal是个柔和的蓝色
color: $--color-nomal
}
}
}
二. 功能的定义
- pageTotal: 总的页数, 就是说比这波数据分成700页显示, 那就传进来700,
- pageSize: 最多显示多少个分页标示, 比如说 传入了3, pageTotal传入了6, 那就是
1,2 ... 6 页面上只显示这三个数. 经过很多次实验, 这个数最小也要传5, 不然体验会很差,最大可以传无限, 朋友们有机会可以自己试试. - value: 实现v-model的基本元素
- validator: 这个函数是子组件接收参数时的校验函数, 这里不能修改参数, 他只负责告诉用户传的对不对就好了, 不要有太多功能, 逻辑分散的话不好维护.
- 下面的代码出现了三个重复的函数, 那么 必须要封装一个共用的工具函数了
pageSize: {
type: Number,
default: 5,
validator: function(value){
if (value < min || value !== ~~value) {
throw new Error(`最小为5的整数`);
}
return true;
}
},
value: {
// 选中页
type: Number,
required: true,
validator: function(value){
if (value < min || value !== ~~value) {
throw new Error(`最小为1的整数`);
}
return true;
}
},
pageTotal: {
// 总数
type: Number,
default: 1,
required: true,
validator: function(value){
if (value < 1 || value !== ~~value) {
throw new Error(`最小为1的整数`);
}
return true;
}
}
抽离工具函数
vue-ui/my/vue-cc-ui/src/assets/js/utils.js
// inspect单词就是检测的意思, 暂时业务只需要传入一个最小值;
export function inspect(min) {
// 返回一个函数作为真正的校验函数
return function(value) {
// 小于这个最小值或不是整数的都要抛错
// ~~这个位运算符的写法的意思就是取整, 取整之后与没取整相等, 当然就不是浮点数
// ~运算符是对位求反,1变0,0变1,也就是求二进制的反码
if (value < min || value !== ~~value) {
throw new Error(`最小为${min}的整数`);
}
return true;
};
}
经过抽离, 我这里就可以化简了, 清爽了很多
pageSize: {
type: Number,
default: 5,
validator: inspect(5)
},
value: {
type: Number,
required: true,
validator: inspect(1)
},
pageTotal: {
type: Number,
default: 1,
required: true,
validator: inspect(1)
}
三. 完善页码的展示(重点)
逐一分析:
- 前面说了, 首尾页码已经直接写上了, 所以比如用户定义的pageSize为5 那么我就要取出中间的3个,
比如用户当前在 第6页, 总页数 12页, 那么 1,...,5,6,7,...,12 中间的567就是我要获取的目标 - 本次选择用计算属性来做, 可以监控v-model的实时变化.名为showPages, 供li去循环展示;
- 兼容value值出现错误的情况
- 这种做法肯定是有偏移的, 比如说 用户输入了pageSize为4, 会出现两种情况让你选择
1,...,5,6,...,12, 1,...,6,7,...,12 在6被激活时, 到底是要5,还是7这个没必要纠结, 随便写一个就好了, 因为我纠结了一下感觉没意义; - 做法思路, 拿到当前要激活的页码value, 然后向他左右延伸, 比如拿到value是 6, 那么左右就是5,7, 这样不断的遍历拿值, 最终在规定数量内, 并且不要触及边界条件.
showPages() {
// 习惯性的定义返回的变量
let result = [],
// 拿到所需的变量
value = this.value,
pageTotal = this.pageTotal,
// 因为要去掉头尾, 所以-2
pageSize = this.pageSize - 2;
// 防止用户输入错误引起的混乱, 比如用户的缓存, 要返给用户, 让用户去处理, 因为很可能v-model出现死循环
if (value > pageTotal) {
// 友好的触发一个错误事件
this.$emit("error", value, pageTotal);
value = pageTotal;
}
// 如果被激活的页面在1与end之间, 则把value放入数组, 不然的话会出现重复值
if (value > 1 && value < pageTotal) result.push(value);
// 左右开弓, 求出当前激活的页码左右的数据
for (let i = 1; i <= pageSize; i++) {
// 加法, 所以检测小于总数就行
if (value + i < pageTotal) {
result.push(value + i);
// 随时甄别是否已经符合条件, 取值已够就退出;
if (result.length >= pageSize) break;
}
// 减法, 只要检测大于1就行
if (value - i > 1) {
result.unshift(value - i);
if (result.length >= pageSize) break;
}
}
return result;
},
上面的li标签 放心遍历了
{{item}}
四. 定义事件
说了这么多, 结构已经做好了, 那么就需要事件的驱动了;
- 这个事件负责通知父级改变值, 同时会做相应的校验;
- 参数为当前想要激活哪一页
- 每次事件都通知父级会有重复的激活, 所以这个方法里面会把想要激活的页码与当前激活的页码进行比较, 放在抛出的事件的第二个参数里面, 用户只要判断isNoChange的真伪就知道是否要请求新数据了, 用户还可以根据这个提示用户"您已在xx页"
handlClick(page) {
if (page < 1) page = 1;
if (page > this.pageTotal) {
page = this.pageTotal;
}
let isNoChange = this.value === page;
this.$emit("input", page);
// 当前值, 与当前值相比是否有变化
this.$emit("onChange", page, isNoChange);
}
- 前进后退直接+1-1就行了
- ...按钮要做一下处理, 因为涉及到前进与后退的加减,
- ...按钮的点击我设计为跳转到一个当前正好看不到的页面, 当前点不到的就行
- 分为两个函数来处理
// 左侧的...
previous() {
// 左侧未显示的第一个
let page = this.showPages[0];
this.handlClick(page - 1);
},
// 右侧的...
next() {
// 右侧未显示的第一个
let len = this.showPages.length,
page = this.showPages[len - 1] + 1;
this.handlClick(page + 1);
},
把时间放到dom上吧
- 1
- ··
- {{item}}
- ··
- {{pageTotal}}
为了判断左右的 ...是否显示, 我们也要抽离出判断的逻辑
比如说中间的那个数组两边的元素连接上了, 就不显示.. 否则出现.
showLeft() {
let { showPages, pageTotal, pageSize } = this;
// 左边不是2, 并且pageTotal超出规定数才显示, 不然1 ... ... 2 很尴尬
return showPages[0] !== 2 && pageTotal > pageSize;
},
showRight() {
let { showPages, pageTotal, pageSize } = this,
len = showPages.length;
return showPages[len - 1] !== pageTotal - 1 && pageTotal > pageSize;
}
至此, 功能性的东西才告一段落
五. 丰富样式与效果
- 用户可以传background 以激活背景色效果, 看对比图
- 对比一下大量数据的美, 哈哈哈, 这个组件的特色就是可以无限多
代码实现一下
// background是关键字, 尤其涉及到css 不要直接使用
// js里面为了方便用户, 可以适当使用
css
单独抽离出ground样式, 为以后的扩展做准备
.ground {
background-color: #f4f4f5;
;
border-radius: 4px;
&:hover {
background-color: $--color-nomal;
color: white;
}
}
.ground-box { // 背景色是关键字
&>li {
@extend .ground;
}
&>.is-active{
background-color: $--color-nomal;
color: white;
}
}
hideOne 属性, 开启只有一页的时候不显示组件
// 最外层的父级定义就好了