工具-无奈之下使用jQuery写了个分页组件

一个文笔一般,想到哪是哪的唯心论前端小白。

前言

提及jQuery,应该很少有新项目在用了吧!但是不可否认的是它在 querySelector 这个API出现之前,几乎所有的前端项目都会或多或少的引入它,从而催生了一系列的前端伪框架和一系列的前端工具。

细想一下 jQuery 时代的编码习惯和现在 Vue/React 的这种编码习惯还是有很大的区别的(纯个人理解):

  1. jQuery 时代大部分的前端工作者在开发业务的时候会关注 dom,一般来说实现一个功能都是先获取页面元素,然后更新或者修改元素;而 Vue 时代被我理解为数据时代,这也是 MVVM 带来的优势,开发习惯已经变成了先处理数据,然后再去关注元素。其实,vue和react已经帮忙实现了第二步。
  2. jQuery 时代,所有的工具几乎都是明文的,开发者可以根据自己的能力,对不满足当前需求的工具进行更改、定制;而 Vue 时代得益于 npm 生态越来越完善,直接修改工具已经变得不太现实,只能联系作者进行更新或者bugfix,所以有时候造轮子也是不得已而为之。
  3. jQuery 时代的开发习惯其实就是典型的 MVC 模式,老的前端可能对这种模式很亲切,而且不管项目多么复杂,都可以看到页面就知道怎么去维护它;而 Vue 时代的项目尤其是到了现在,微前端、多包结构、ts、webpack等等新奇的技术如雨后春笋般拔地而起,甚至有些项目运行起来都很麻烦。

说远了,收!

这次记录的只是一个分页组件的开发过程,起因是因为一个朋友在原来的没有引入任何UI库的情况下要新增一个带有分页的tab页面,,虽然没什么难点,但是确实实实在在的绕了我一个小时的时间,略有所得,所以简单记录一下!

效果如下:

思路

网上有分页组件,但是很少有单独的一个分页组件。花了5分钟从网上找,看了一下,感觉如果拉来再改改ui样式什么的,应该跟我自己写一个差不多,关键是时间还比较充足!

需求分析:

  1. 包含总共条数 total,total 为 0 时状态需要注意,里面的一些计算会异常
  2. 可以自己选择每页条数 size ,最好支持自定义,或者方便修改
  3. 首页/末页 可以自己跳转到 第一页/最后一页
  4. 上一页/下一页,注意边界就行
  5. 三个点的按钮支持显示按钮的翻页,其实就是跳页,有5个按钮就一下跳5页,超出边界则定位边界值
  6. 中间数字按钮,临近边界值则不切换按钮内容,中间值正常显示,总页数少于5则只显示页数个按钮
  7. 各个按钮显示隐藏条件处理

小小的一个分页组件需要注意的点还挺多!而且一大堆按钮和一大堆交互!

开发过程

使用方式设计

关于使用方式这里我确实是思考了有几分钟的:

  1. new Pagenation(),使用 工厂模式或者单例模式将其封装成一个分页类,可以有自己的状态,各个方法也不会污染到全局,。
  2. $().pagenation(),毫无疑问这是最适合 jQuery 项目的调用方式,同时也可以支持链式调用实现后续的其他操作,但是需要增加全局对象来保存分页属性。
  3. 简单粗暴,直接使用html去写,通过控制所有按钮的显示隐藏的方式来实现,速度肯定是最快的,但是复用性太低了。
  4. 在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 布局

常见的这种排排站的按钮布局方式很多:

  1. 使用 div 配合 float:left ,通过修改 float 的值改变左右对齐方式,但是需要注意顺序
  2. 使用 flex 布局,将容器排列方向改成横向 flex-direction: row;
  3. 所有的交互按钮都用行内标签,通过 text-align 来控制靠左和靠右,或者居中

如何独立出来并和table交互

首先,这是两个问题:

  1. 如何独立出来,即可以单独放在任何地方使用,和其他组件没有耦合度。
  2. 和table交互,即和页面其他的组件(方法)通信。

一个个来说。

解耦

想要实现解耦,就要满足几个条件:

  1. 自己拥有自己的状态,并且能够实时的更新自己的状态
  2. 自己的状态更新时不会影响到页面其他的功能,但是其他功能能随时获取到它的状态
  3. 所有的元素、样式由自己控制,不受外界任何因素影响
  4. 所有的方法都属于自己的方法,不会污染全局。

其实核心就是状态,我之所以用一个小时的时间去实现,其中大半的时间都是在改状态的控制方式,甚至一度想使用全局变量来实现,或者推翻技术方案,使用 class 来做。最后选择了在 $(this) 上增加一个 属性 data-page,并以此作为它的状态。

剩下的只需要注意不重名就好了!

通信

通信分为两个方向:

  1. 当用户操作分页组件时,要触发外界的的获取数据逻辑
  2. 当外界的查询或其他操作修改分页数据时,分页组件能够实现实时更新

这里的实现方式就是 data-page,在每次调用 $().pagenation() 时,传入两个参数:

  1. page,类型为 object,包含三个字段:current、size、total
  2. 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()
 })

搜索时刷新表格并重置分页

为了展示具体的使用方式,这里展示两段代码:

  1. 搜索方法:重置分页,用新的分页数据,调用表格刷新事件
  2. 表格加载方法:数据回来以后根据 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">&lt;</button>`
            var _next = `<button id="pagenationNextPage">&gt;</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端项目上确实非常常用,而且各个框架已经封装的很好了,而且只有一个小页面使用,所以就没有很花功夫去优化它。

简单总结一下所得:

  1. 得到一个基于 jQuery 开箱即用的分页组件。
  2. 得到一个 给定 1-n 之间的数字,并基于这个数字,向两边查找满足最大长度l的数组的递归函数。
  3. 温习了一下 jQuery 的扩展流程以及 一些 jQuery 的一些常见不常见的API的用法。
  4. 对于 MVC 和 MVVM 的设计模式有了进一步的认知。
  5. 各种js设计模式需要更熟练的运用,有些模式要优于对 jQuery 的扩展。
相关推荐
阿伟来咯~17 分钟前
记录学习react的一些内容
javascript·学习·react.js
吕彬-前端22 分钟前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱25 分钟前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
guai_guai_guai34 分钟前
uniapp
前端·javascript·vue.js·uni-app
也无晴也无风雨35 分钟前
在JS中, 0 == [0] 吗
开发语言·javascript
bysking2 小时前
【前端-组件】定义行分组的表格表单实现-bysking
前端·react.js
王哲晓2 小时前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
fg_4112 小时前
无网络安装ionic和运行
前端·npm
理想不理想v2 小时前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
酷酷的阿云2 小时前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js