最近在做部门的项目时,有几个页面都是展示分页列表,页面中有专门的一块搜索区域。在写这些页面时,发现有很多通用的逻辑,这些通用逻辑的代码量大概 100~200 行,细节之处还是挺多的,容易出错。
如果能把他们抽象出来,做一个通用列表组件,那么之后再有相关页面要开发,就能节省很多代码同时大大提升开发效率。本文就是记录我在写这个组件时的思路以及详细技术细节。
组件的源码我已经放在了github
上:jquery-common-paginator.js。 可以参见其中的test.html
来学习如何使用。
抽象
一般来说一个带搜索的分页列表页面布局类似如下:
其中:
搜索条件区域是每个页面所独有的;
列表展示区域虽然也不一样,但是稍加思考,就会发现真正不一样的是每一个列表项的展示逻辑。整个列表区域只不过是迭代这个逻辑,最后将所有结果拼接起来渲染,这个迭代并拼接的过程就是通用的逻辑;
分页处理区域的逻辑是通用的:
- 在点击页码时,利用当前搜索条件加上每页大小,来搜索对应页的数据。同时页码的展示样式也应当比较固定。
- 分页的提示信息基本也是相同的,在同个项目组,这些信息应当是比较固定的
- 在更改每页显示条数时,需要利用当前搜索条件及新的每页大小来搜索第一页的数据
其他通用逻辑
- 删除当前页的某条数据时,若当前页只剩一条数据,此时需要获取上一页的数据,同时页面选中的页码减 1;如果当前页还有多条数据,直接刷新当前列表
- 删除当前页所有数据时,需要获取上一页的数据
- 更新当前页数据时,若列表是以更新时间倒序排列的,那么更新后,页面需要跳转到第一页;若列表是以其他非时间敏感字段排序的,那么只需重新渲染当前页即可
我们的通用列表组件要做的就是封装这些通用逻辑,这样用户只用专注于:
- 如何生成后台请求的搜索条件参数
- 如何渲染每一个列表项
如果想要列表保持灵活性,还需要让用户可以自定义如下配置:
- 如何使用搜索条件参数+分页参数生成真正的后台接口请求参数
- 如何发送请求
- 如何从响应中提取列表数据
- 如何从响应中提取数据总条数
- 如何处理失败
- 每页大小的数组
- 分页区域的渲染逻辑
第一步
我设想中的组件是需要使用构造函数进行实例化,构造函数名就叫JqueryCommonPaginator
,那么用户就会使用如下的代码进行初始化:
1 | var paginator = new JqueryCommonPaginator(); |
我们希望用户进行选项配置时更简单,那么就提供一组默认配置,同时在初始化时给用户一个覆盖所有配置的机会:
1 | function JqueryCommonPaginator(options) { |
接下来我们就的重点关注如何编写默认逻辑defaultOptions
默认配置defaultOptions
此处的默认配置是基础我们项目的前后台请求规范编写的。
基本配置
1 | /** |
生成真正后台请求入参
我们项目中的接口入参格式均是如下这样:
1 | { |
1 | /** |
获取后台数据接口
请求的 url 是一个由userParam.asistParam.path
决定的反向代理路径。
1 | /** |
从响应中获取列表数据
我们项目的接口响应格式是:
1 | TODO: 添加响应格式; |
1 | defaultOptions.getListFromResponse = function(response) { |
从响应数据中获取总数据条数
1 | defaultOptions.getTotalCountFromResponse = function(response) { |
根据每个列表项数据生成对应的模板 HTML
此函数在不同页面必然是不同的,所以这里仅仅做一个示范。 函数的要求是返回一个字符串:
1 | defaultOptions.itemRenderFunc = function(itemModel, renderOptionFunc) { |
注意到这个函数有一个renderOptionFunc
参数,执行此函数会得到一个额外对象,此对象的作用是提供itemModel
不能提供的额外数据,譬如页面的URL
等等。当然如果不需要额外参数,直接忽略此参数即可。
提供给@itemRenderFunc 的参数
作用如上所述。此函数如果需要返回值,那么基本是不同页面不一样。
1 | defaultOptions.itemRenderOptionFunc = function(itemModel) { |
当请求成功,但列表为空时,渲染空白的列表区域
1 | defaultOptions.renderEmptyList = function() { |
渲染分页区域
1 | /** |
注意这里的$("").createPage
方法,这是部门另一位同事写的一个jquery
插件。它可以帮助我们渲染页码条,同时提供一个选项来让我们注册页码点击事件。具体源码可以参见github
源码中的jquery.page.js
。
覆盖默认配置
可以看到defaultOptions
中的默认配置还是挺多的,如果需要覆盖每一个配置,那么比较好的做法就是提供一一对应的set
方法,例如:
1 | JqueryCommonPaginator.prototype.setPageSizeList = function(newPageSizeList) { |
它们的逻辑基本是相同的,即使用函数中提供的参数来替换defaultOptions
的对应属性成员,同时校验参数类型。为此我们可以提供一个帮助函数,所有set
方法均调用此函数:
1 | /** |
注意这里的Object.prototype.toString.call(newOptionValue)
,它的返回值是一个字符串,格式诸如"[object Object]"
,这是JavaScript
的一个比较完善的类型检测技巧。其他检测类型的方式还有typeof
和instanceof
,但typeof
只能检测基本值类型,instanceof
缺乏通用性只能确定是否为某个特定的类型,而且涉及到继承时还有出现一些意料之外的情况。
有了这个帮助函数之后,其他set
方法只用简单调用它:
1 | JqueryCommonPaginator.prototype.setPageSizeList = function(newPageSizeList) { |
稍微细心点可以发现这些set
都是返回this
,这是为了方便链式调用,例如:
1 | paginator.setPageSizeList(mockPageSizeList).setDomSelectors(mockDomSelectors); |
通用逻辑
好了,上面介绍那么多,终于可以介绍通用逻辑的处理了。
成功拿到想要的 response 后的操作
这里只需拿到列表数据后,只需渲染列表和分页区域。
1 | function _successFunc(response) { |
使用call
是为了显示指定this
的值,否则默认的this
就是this.options
了。
渲染列表区域
上面说了,单条数据项的渲染是使用itemRenderFunc
决定的,我们只需迭代这个过程即可,返回最终拼接的 HTML,并最终渲染:
1 | function _renderList(dataList) { |
this.options.DOM_SELECTORS.LIST_SELECTOR
是页面中渲染列表区域的DOM
选择器,还有其他几种选择器:
1 | // 相关的页面dom选型器。用户可自定义 |
绑定每页大小变更事件
只需在改变每页大小后,使用新的页面大小重新获取第一页数据即可。
1 | function _bindPageSizeChangeHandler() { |
此方法会在_successFunc
渲染完分页区域后被调用。
_refresh
也是一个帮助方法,用于根据当前的参数获取数据并刷新列表。
1 | function _refresh() { |
列表项变更操作
通常渲染完列表后,可能会对列表项进行各种操作,可以归纳为删除和更新操作。 这两种操作的逻辑其实也是通用的。
删除:
- 若当前页只剩一条数据,此时需要获取上一页的数据,同时页面选中的页码减 1;
- 若当前页还有多条数据,直接刷新列表。或者可以优化一下,不是刷新整个页面,而是只删掉第 index 条,同时将下一页的第一条放到当前页末尾,同时提供可选的动画效果。
更新:
- 若列表是以更新时间倒序排列的,那么更新此条时,页面需要跳转到第一页
- 若列表是以其他非时间敏感字段排序的,那么只需重新渲染当前页即可
除了单条变更,有时还会有批量的变更操作,例如批量删除、批量更新,他们的原理和单条操作大同小异。
单条删除
1 | /** |
删除整页
1 | /** |
更新单条
1 | /** |
更新整页
1 | /** |
除此之外,如果想批量操作指定的多条数据,原理大同小异,在此就不赘述了。
总结
本文详细阐述了如何写一个 pc 端的分页列表,给出了总体思路和详细技术方法,并提供具体代码供读者参考。