一个文笔一般,想到哪是哪的唯心论前端小白。
前言
提及jQuery,应该很少有新项目在用了吧!但是不可否认的是它在 querySelector
这个API出现之前,几乎所有的前端项目都会或多或少的引入它,从而催生了一系列的前端伪框架和一系列的前端工具。
细想一下 jQuery 时代的编码习惯和现在 Vue/React 的这种编码习惯还是有很大的区别的(纯个人理解):
- jQuery 时代大部分的前端工作者在开发业务的时候会关注 dom,一般来说实现一个功能都是先获取页面元素,然后更新或者修改元素;而 Vue 时代被我理解为数据时代,这也是 MVVM 带来的优势,开发习惯已经变成了先处理数据,然后再去关注元素。其实,vue和react已经帮忙实现了第二步。
- jQuery 时代,所有的工具几乎都是明文的,开发者可以根据自己的能力,对不满足当前需求的工具进行更改、定制;而 Vue 时代得益于 npm 生态越来越完善,直接修改工具已经变得不太现实,只能联系作者进行更新或者bugfix,所以有时候造轮子也是不得已而为之。
- jQuery 时代的开发习惯其实就是典型的 MVC 模式,老的前端可能对这种模式很亲切,而且不管项目多么复杂,都可以看到页面就知道怎么去维护它;而 Vue 时代的项目尤其是到了现在,微前端、多包结构、ts、webpack等等新奇的技术如雨后春笋般拔地而起,甚至有些项目运行起来都很麻烦。
说远了,收!
这次记录的只是一个分页组件的开发过程,起因是因为一个朋友在原来的没有引入任何UI库的情况下要新增一个带有分页的tab页面,,虽然没什么难点,但是确实实实在在的绕了我一个小时的时间,略有所得,所以简单记录一下!
效果如下:
思路
网上有分页组件,但是很少有单独的一个分页组件。花了5分钟从网上找,看了一下,感觉如果拉来再改改ui样式什么的,应该跟我自己写一个差不多,关键是时间还比较充足!
需求分析:
- 包含总共条数 total,total 为 0 时状态需要注意,里面的一些计算会异常
- 可以自己选择每页条数 size ,最好支持自定义,或者方便修改
- 首页/末页 可以自己跳转到 第一页/最后一页
- 上一页/下一页,注意边界就行
- 三个点的按钮支持显示按钮的翻页,其实就是跳页,有5个按钮就一下跳5页,超出边界则定位边界值
- 中间数字按钮,临近边界值则不切换按钮内容,中间值正常显示,总页数少于5则只显示页数个按钮
- 各个按钮显示隐藏条件处理
小小的一个分页组件需要注意的点还挺多!而且一大堆按钮和一大堆交互!
开发过程
使用方式设计
关于使用方式这里我确实是思考了有几分钟的:
- new Pagenation(),使用 工厂模式或者单例模式将其封装成一个分页类,可以有自己的状态,各个方法也不会污染到全局,。
- $().pagenation(),毫无疑问这是最适合 jQuery 项目的调用方式,同时也可以支持链式调用实现后续的其他操作,但是需要增加全局对象来保存分页属性。
- 简单粗暴,直接使用html去写,通过控制所有按钮的显示隐藏的方式来实现,速度肯定是最快的,但是复用性太低了。
- 在3的基础上,进行一次简单封装,避免太多的方法名,导致有方法名命名冲突。指标不治本。
最终选择方案2,主要是想回忆一下 jQuery 扩展插件的流程,到最后发现 方案1 才是最合适的。
技术方案设计
1. js 结构
既然选择了方案2,那么具体方式就是针对 JQuery 扩展一个方法出来,常用的方法有两个:$.fn.extend()
和 $.extend = fn
。
即代码结构为:
js
$.fn.extend({
pagenation: function(){
// todo pagenation
}
})
2. html 结构
众所周知,jQuery 最方便的使用方式就是只写一个容器,其他的都有组件内部实现,所以我也是这写的:
html
<div class="pagenation"></div>
3. css 布局
常见的这种排排站的按钮布局方式很多:
- 使用 div 配合
float:left
,通过修改float
的值改变左右对齐方式,但是需要注意顺序 - 使用 flex 布局,将容器排列方向改成横向
flex-direction: row;
- 所有的交互按钮都用行内标签,通过
text-align
来控制靠左和靠右,或者居中
如何独立出来并和table交互
首先,这是两个问题:
- 如何独立出来,即可以单独放在任何地方使用,和其他组件没有耦合度。
- 和table交互,即和页面其他的组件(方法)通信。
一个个来说。
解耦
想要实现解耦,就要满足几个条件:
- 自己拥有自己的状态,并且能够实时的更新自己的状态
- 自己的状态更新时不会影响到页面其他的功能,但是其他功能能随时获取到它的状态
- 所有的元素、样式由自己控制,不受外界任何因素影响
- 所有的方法都属于自己的方法,不会污染全局。
其实核心就是状态,我之所以用一个小时的时间去实现,其中大半的时间都是在改状态的控制方式,甚至一度想使用全局变量来实现,或者推翻技术方案,使用 class 来做。最后选择了在 $(this)
上增加一个 属性 data-page
,并以此作为它的状态。
剩下的只需要注意不重名就好了!
通信
通信分为两个方向:
- 当用户操作分页组件时,要触发外界的的获取数据逻辑
- 当外界的查询或其他操作修改分页数据时,分页组件能够实现实时更新
这里的实现方式就是 data-page
,在每次调用 $().pagenation() 时,传入两个参数:
- page,类型为 object,包含三个字段:current、size、total
- handler,类型为 function,为分页状态发生改变时,要触发的方法
这样一来,组件可以使用传入的page来渲染分页的各个按钮,同时点击各个按钮的时候,也能触发外界的方法了。同时外界也可以通过获取 data-page
来读取内部状态。
以 data-page
为媒介实现了组件之间的通信。
查询或重置时如何重置分页
如通讯那段所示,重置分页,只需要将page设为默认值就好了。
代码分享
html 部分
就只需要一个容器就好了,和调用的保持一致。
html
<div class="pagenation"></div>
css 部分
这里缺少了全局的样式,例如我最常用的: *{margin:0;padding:0;}
css
:root {
--boder: 1px solid #ebecee;
}
/* pagenation */
.pagenation-active {
background: rgb(197, 197, 253);
}
.pagenation {
line-height: 40px;
height: 40px;
width: 100%;
padding: 10px;
}
.pagenation select,
.pagenation button {
margin-left: 6px;
}
.pagenation select {
height: 32px;
width: 80px;
border: var(--boder);
padding: 0 6px;
border-radius: 4px;
}
.pagenation button {
height: 32px;
outline: none;
min-width: 32px;
border: var(--boder);
border-radius: 4px;
cursor: pointer;
}
js 部分
这里是重点,需要拆分成三部分:
初始化页面
注意,一定要先初始化分页的 data-page
属性,然后再加载表格数据,因为当前容器上是没有 data-page
属性的,加载表格时需要用到 分页的数据,进行表格数据的获取。
js
$(function () {
$('.pagenation').attr('data-page', JSON.stringify({ current: 1, size: 10, total: 0 }))
reloadTable()
})
搜索时刷新表格并重置分页
为了展示具体的使用方式,这里展示两段代码:
- 搜索方法:重置分页,用新的分页数据,调用表格刷新事件
- 表格加载方法:数据回来以后根据 total 会刷新分页。
js
// 搜索,只需要修改当前选择页就好了,分页条数不用管
function handleSearch() {
const page = JSON.parse($('.pagenation').attr('data-page'))
page.current = 1
$('.pagenation').attr('data-page', JSON.stringify(page))
reloadTable()
}
// 加载表格
function reloadTable() {
// 获取表格数据
const page = JSON.parse($('.pagenation').attr('data-page'))
// 获取当前选中的 tab
const attr = $('.contract-tab-bar-item-active').attr('data-attr')
// 获取查询条件
const query = {
contractNumber: $('#contractNumber').val(),
contractTime: $('#contractTime').val(),
contractType: $('#contractType').val(),
}
// TODO 请求表格数据
// 接口返回 total
const data = [
{
k1: parseInt(Math.random() * 1000),
k2: parseInt(Math.random() * 1000),
k3: parseInt(Math.random() * 1000),
k4: parseInt(Math.random() * 1000),
k5: parseInt(Math.random() * 1000),
k6: parseInt(Math.random() * 1000),
k7: parseInt(Math.random() * 1000),
k8: parseInt(Math.random() * 1000),
k9: parseInt(Math.random() * 1000),
}
]
const htm = data.map(item => {
console.log(item);
return `<td>${item['k1']}</td>
<td>${item['k2']}</td>
<td>${item['k3']}</td>
<td>${item['k4']}</td>
<td>${item['k5']}</td>
<td>${item['k6']}</td>
<td>${item['k7']}</td>
<td>${item['k8']}</td>
<td>${item['k9']}</td>
<td><button>取消</button></td>`
}).join('')
$('.contract-table-wrap table tbody').html(htm)
// 模拟 total
page.total = 1004
// 刷新 分页组件
$('.pagenation').pagenation(page, reloadTable)
}
组件核心代码
这套代码后续如果复用我可能会优化,优化点已经标注出来了,只要入参都正确,是不会有什么问题的。
感兴趣的可以简单优化一下,不求人。
js
// pagenation.js
$.fn.extend({
pagenation: function (config, handler) {
// 简单齐整一下 config,其实还需要增加一下判断,对 异常入参进行处理
config = {
current: parseInt(config.current) || 1,
size: parseInt(config.size) || 10,
total: parseInt(config.total) || 0
}
console.log('config >>', config);
// 各个小模块
var _total = `<span>共 ${config.total} 条</span>`
// 下拉菜单,可以在这里配置
var _size = `<select name="pageSize" id="pagenationPageSize" >
${[10, 20, 30, 50, 100].map(item => `<option value="${item}">${item}条/页</option>`).join('')}
</select>`
var _first = `<button id="pagenationFirstPage">首页</button>`
var _end = `<button id="pagenationEndPage">末页</button>`
var _last = `<button id="pagenationLastPage"><</button>`
var _next = `<button id="pagenationNextPage">></button>`
// 计算总共可以分多少页
var pageLength = config.total % config.size === 0 ? parseInt(config.total / config.size) : parseInt(config.total / config.size) + 1
// 工具函数,从 current 开始,向两侧查询,最多查到5个
function findByCenter(center, length, max = 5) {
var left = true, right = true, step = 1, result = [center]
// 偷懒,使用了一个好多入参的递归
function recur(left, right, length, step, center, result, max) {
// 左边界处理
if (center - step > 0) {
left = true
} else {
left = false
}
// 右边界处理
if (center + step <= length) {
right = true
} else {
right = false
}
// 左边的往前插,使用 unshift 方法
if (left) {
result.unshift(center - step)
}
// 右边的往后插,使用 push 方法
if (right) {
result.push(center + step)
}
// 如果两边都没了,直接返回
if (!right && !left) {
return result.length > 0 ? result : [1]
}
// 判断继续递归还是跳出
if (result.length >= max) {
return result
} else {
step++
return recur(left, right, length, step, center, result, max)
}
}
// 返回结果
return recur(left, right, length, 1, center, result, max)
}
// 数字按钮列表
var _btnArr = findByCenter(config.current, pageLength)
// 三个点 按钮
var _lastPoint = _btnArr[0] > 1 ? `<button id="pagenationLastPoint">···</button>` : ''
var _nextPoint = _btnArr.toReversed()[0] < pageLength ? `<button id="pagenationNextPoint">···</button>` : ''
// 渲染按钮列表,current 需要高亮,增加 active 类
var _btns = _lastPoint + _btnArr.map(item => item === config.current ?
`<button id="pageButton" class="pagenation-number pagenation-active">${item}</button>` :
`<button id="pageButton" class="pagenation-number">${item}</button>`).join('') + _nextPoint
// 将所有的dom拼接进容器
$(this).html(_total + _size + _first + _last + _btns + _next + _end)
// 注意下面的方法需要在html都渲染之后,及$().html() 之后,不然是拿不到的,这个和 原生 js 的createElement 是不一样的。
// 设置每页条数数据
$('#pagenationPageSize').val(config.size)
// 使用 _self 保存当前节点,在 on 方法内部,$(this) 的指向会变
var _self = $(this)
// 每页条数改变
$('#pagenationPageSize').on('change', function () {
config.size = $(this).val()
config.current = 1
console.log('config.size >> ', config.size);
// 之所以没有将 handler 放在操作外面,大家可以试一下!无限调用哦~~~
_self.pagenation(config)
handler() // 就是这里触发回调,所以入参需要校验 handler
})
$('#pagenationFirstPage').on('click', function () {
config.current = 1
_self.pagenation(config)
handler()
})
$('#pagenationEndPage').on('click', function () {
config.current = pageLength
_self.pagenation(config)
handler()
})
$('#pagenationLastPage').on('click', function () {
config.current = config.current - 1 < 1 ? config.current : config.current - 1
_self.pagenation(config)
})
$('#pagenationNextPage').on('click', function () {
config.current = config.current + 1 > pageLength ? config.current : config.current + 1
_self.pagenation(config)
handler()
})
$('#pagenationLastPoint').on('click', function () {
config.current = config.current - 5 < 1 ? 1 : config.current - 5
_self.pagenation(config)
handler()
})
$('#pagenationNextPoint').on('click', function () {
config.current = config.current + 5 > pageLength ? pageLength : config.current + 5
_self.pagenation(config)
handler()
})
$(this).find('.pagenation-number').each(function () {
$(this).on('click', function () {
config.current = $(this).html()
console.log($(this).html());
_self.pagenation(config)
handler()
})
})
// 左右按钮禁用逻辑
if (config.current === pageLength) {
$('#pagenationNextPoint').attr('disabled', 'disabled')
} else {
$('#pagenationNextPoint').attr('disabled', false)
}
if (config.current === 1) {
$('#pagenationLastPoint').attr('disabled', 'disabled')
} else {
$('#pagenationLastPoint').attr('disabled', false)
}
$(this).attr('data-page', JSON.stringify(config))
},
})
后记
如上就是本次分享的内容了,所有的代码应该开箱即用,除了css那段需要增加全局margin和padding的清楚。
分页组件在B端项目上确实非常常用,而且各个框架已经封装的很好了,而且只有一个小页面使用,所以就没有很花功夫去优化它。
简单总结一下所得:
- 得到一个基于 jQuery 开箱即用的分页组件。
- 得到一个 给定 1-n 之间的数字,并基于这个数字,向两边查找满足最大长度l的数组的递归函数。
- 温习了一下 jQuery 的扩展流程以及 一些 jQuery 的一些常见不常见的API的用法。
- 对于 MVC 和 MVVM 的设计模式有了进一步的认知。
- 各种js设计模式需要更熟练的运用,有些模式要优于对 jQuery 的扩展。