共计 6423 个字符,预计需要花费 17 分钟才能阅读完成。
第九集: 从零开始实现(分页器组件)
本集定位:
分页器这个组件也算是个老朋友了, 还记得刚学 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 循环的数据, 也就是本集的重点了.
<template>
<div class='cc-pagination'>
<button class="btn-prev">
<slot name='previous'> < </slot>
</button>
<ul class='cc-pagination__box'>
<li>1</li>
<li>··</li>
<li ....>{{item}}</li>
<li>··</li>
<li v-if="总页数!== 1">{{总页数}}</li>
</ul>
<button class="btn-prev">
<slot name="next"> > </slot>
</button>
</div>
</template>
先展示一下基本的样子
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 标签 放心遍历了
<li v-for="item in showPages"
:key='item'
:class="{'is-active':value === item}">{{item}}</li>
四. 定义事件
说了这么多, 结构已经做好了, 那么就需要事件的驱动了;
- 这个事件负责通知父级改变值, 同时会做相应的校验;
- 参数为当前想要激活哪一页
- 每次事件都通知父级会有重复的激活, 所以这个方法里面会把想要激活的页码与当前激活的页码进行比较, 放在抛出的事件的第二个参数里面, 用户只要判断 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 上吧
<template>
<div class='cc-pagination'>
<button class="btn-prev"
@click="handlClick(value-1)"
// 到头了要提示用户, 显示出禁止点击的样式
:style="{'cursor': (value === 1)?'not-allowed':'pointer'}">
<slot name='previous'> < </slot>
</button>
<ul class='cc-pagination__box'>
<li @click="handlClick(1)"
:class="{'is-active':value === 1}">1</li>
<li v-if='showLeft'
@click="previous">··</li>
<li v-for="item in showPages"
:key='item'
@click="handlClick(item)"
:class="{'is-active':value === item}">{{item}}</li>
<li v-if='showRight'
@click="next">··</li>
<li v-if="pageTotal !== 1"
@click="handlClick(pageTotal)"
:class="{'is-active':value === pageTotal}">{{pageTotal}}</li>
</ul>
<button class="btn-prev"
@click="handlClick(value+1)"
// 到头了要提示用户, 显示出禁止点击的样式
:style="{'cursor': (value === pageTotal)?'not-allowed':'pointer'}">
<slot name="next"> > </slot>
</button>
</div>
</template>
为了判断左右的 … 是否显示, 我们也要抽离出判断的逻辑
比如说中间的那个数组两边的元素连接上了, 就不显示.. 否则出现.
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 里面为了方便用户, 可以适当使用
<ul class='cc-pagination__box'
:class="{'ground-box':background}">
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 属性, 开启只有一页的时候不显示组件
// 最外层的父级定义就好了
<div class='cc-pagination'
v-if='!(hideOne && pageTotal === 1)'>
total: 开启左侧显示条数模式
我做的与别人不同, 你传了我就显示, 没传就无所谓, 没有附加的功能
<p v-if="total"
class="total-number"> 总共 <span> {{total}}</span> 条 </p>
麻雀虽小, 五脏俱全, 做这个也花费了半天的时间, 测出好多问题, 都一一改进了.
end
另外最近计划做一个 vue,vuex, vue-router, webpack 原理解析的系列, 也是一点点从零开始, 期待大家继续一起学习, 一起进步, 实现自我价值!!
下一集准备聊聊 计数器 … 上期就这么说的????;
更多好玩的效果请关注个人博客: 链接描述
github 地址: 链接描述