山地人

第010期.前端问答-分页功能的那点事

山地人
山地人
2021-05-13

小伙伴们这两天,在群里问组件封装和分页相关的问题。接下来2期我们来讨论两个问题:

  1. 分页的功能怎么做?
  2. 如何封装可以复用的分页组件?

art-book-pages-browse-256467

一、分页

1.1 分页需求的由来

要谈分页,我们先得知道为什么会有分页的需求。我们在日常产品开发过程中,经常会碰到表格类的数据。对于现在ajax盛行前后端分离的时代,这些数据往往是客户端(比如:浏览器)发送一条ajax请求给我们的后台服务器。然后后代服务器查询出对应的数据,按照一定的格式组织好再返回给客户端。

如果数据不多,这种方式就能很好的工作。可是问题来了,当后台的记录慢慢增多,比如对于一个购物网站。后台的订单信息越来越多。当我们开发一个订单管理系统时,如果数据库里有几十万条数据。我们的浏览器无法一次接收这么多数据并显示。而且就算能显示,用户也不能看的了这么多记录。于是——分页的需求就诞生了。

1.2 分析分页需求

不分页的时代,我们一次从数据库里查询出全部需要的数据,然后发送一个json格式的数据给前端

{
code:0,
msg:"ok",
data:[
{ id:"1001", name:"香蕉" },
{ id:"1002", name:"苹果" },
...
]
}

到了分页的时代,我们需要把这些数据切分成一页一页的数据。比如原来数据库有100条数据,我们以10条为一页的容量,把这100条记录分为10条一页,一共分割了10页。

好,通过上面的描述,我们来归纳一下分页所产生的一些信息:

  1. 数据库里查询出的符合条件的记录总数(total)
  2. 分页的大小(page-size):也就是我们一页可以放几条数据
  3. 分页的数量(page-count):分页后一共产生了多少个页面
  4. 当前要取第几页的数据(current-page)

然后考虑一下,客户端要向后端请求分页数据需要知道哪些必要的信息:

  1. 数据库里有多少符合条件的记录(total)?
  2. 我需要的分页大小(page-size)是多少?
  3. 我现在要取第几页(current-page)的数据?

最后,我们设计一下API请求的格式:

//请求参数
let currentPage=0 //我们以0位起始索引,请求第一个页数据
//服务器返回的数据
{
code: 0,
msg: "ok",
data: {
currentPage: 0,
pageSize: 10,
total: 100,
orders: [
{ id:"1001", name:"香蕉" },
{ id:"1002", name:"苹果" },
...
]
}
}

1.3 实现分页功能

下面我们来实现后台API,为了简化操作,我们后台使用静态数据作为我们的数据库。

1.3.1 后台API实现

新建一个server文件夹,然后初始化我们的工程

mkdir server
cd server
npm init -y

安装本次后台需要用到的第三方依赖,我们这次还是继续使用我们熟悉的express。另外我们使用nodemon来实现代码更新自动重启

npm i -S express
npm i -D nodemon

创建db.json文件,用来模拟我们的数据库里的orders表,内容格式参考项目代码

创建server.js文件

var db = require("./db.json")
// 定义查询订单总量的API
function getOrderTotalCount(){
return db.orders.length;
}
// 定义根据pageSize页面大小和currentPage当前页查询分页订单信息的API
function getOrders(pageSize,currentPage){
const start = pageSize*currentPage;
const end = start+pageSize;
return db.orders.slice(start,end);
}

服务端订单查询的API

app.get('/orders/:pageSize/:pageNumber',function(req,res,next){
const pageSize = Number(req.params.pageSize || 5);
const pageNumber = Number(req.params.pageNumber || 0);
console.log('pageSize:', pageSize);
console.log('pageNumber:', pageNumber);
const orders = getOrders(pageSize,pageNumber);
const data = {
currentPage: pageNumber,
pageSize: pageSize,
total: getOrderTotalCount(),
orders: orders,
}
res.send( resultSuccess(data) )
})

我们定义的API接口为/orders/:pageSize/:currentPage。这里简要说明一下

如果我们收到的客户端请求为 /orders/5/0 表示的含义就是

:pageSize = 5 客户端需要一个分页大小为5条数据

:currentPage = 0 起始分页索引为0

也就是查询一页长度为5条记录的分页表的第一页的数据。

在package.json中配置一条scripts

"scripts": {
"start": "nodemon server.js"
}

然后就可以启动服务器测试我们的服务器api了

npm run start

打开浏览器,发送 http://localhost:3000/orders/5/0 请求,如果没有任何异常我们就得到了下面的结果

{
"code":0,
"msg":"ok",
"data":{
"currentPage":0,
"pageSize":5,
"total":15,
"orders":[
{"id":"1001","name":"苹果"},
{"id":"1002","name":"香蕉"},
{"id":"1003","name":"菠萝"},
{"id":"1004","name":"芒果"},
{"id":"1005","name":"李子"}
]
}
}

服务端的完整代码参考server.js文件。点开这里查看

1.3.2 前端功能实现(Vue+Element版)

img

这一次,我们前端采用 Vue+Element方式来构建我们的页面。为了简化代码我这里使用了一个HTML的单页面,没有引入任何工程相关的代码。

在服务器的public目录下创建一个index.html,写好页面结构相关代码。

准备data数据

在Vue实例的data部分,我们需要保存几个信息:

new Vue({
el: '#app',
data() {
return {
orders: [],
currentPage: 0,
pageSize: 5,
total: 0,
};
}
});

orders在初始时,没有任何内容所以是一个空数组,初始化最开始的currentPage为0,分页大家pageSize设置为5,总的order数量最开始未知初始为0。

封装请求服务器的API

在methods部分添加一个fetchOrders的API用于请求服务器/orders/5/0获取数据,这里我采用了es6的fetch来请求服务器数据

fetchOrders(currentPage=0){
fetch("/orders/"+this.pageSize+"/"+currentPage)
.then((res)=>{
return res.json();
}).then((res)=>{
console.log(res.data);
this.orders = res.data.orders;
this.currentPage = res.data.currentPage;
this.total = res.data.total;
});
}

当API获取数据后,直接把data里的三个数据重新更新掉。剩下的页面变化完全交给Vue自己去处理。这也就是我们现在常说的数据驱动的开发方式。我们只专注于数据维护,渲染方面的具体内容由Vue帮我们维护。

封装页面

有了请求和数据,最后我们需要定义的就是拿到数据我们怎么来显示。我们需要把显示的规则告诉Vue。

<div id="app">
<el-table :data="orders" border style="width: 100%">
<el-table-column prop="id" label="单号"></el-table-column>
<el-table-column prop="name" label="名称"></el-table-column>
</el-table>
<el-pagination
background
class="pagination"
:page-size="pageSize"
:total="total"
@current-change="handleChangePageInfo"
>
</el-pagination>
</div>

这里我们使用了Element,所以按照Element的官方文档的规则去使用el-tableel-pagination两个组件。

el-table组件中,我们只需要告诉他数据data是什么,这里是我们在data中定义的orders

el-table中的每一列我们使用内嵌el-table-column来指定。

单号列映射到orders里的id字段prop=“id”

名称列映射到orders的name字段prop=“name”

对于分页,我们使用el-pagination组件

这个组件的使用也非常简单,只需要指定总条数totalpage-size每页的分页大小就可以了。

最后在点击分页时,我们需要通知API重新发起请求,利用el-paginationcurrent-change时事件。绑定handleChangePageInfocurrent-change上。

handleChangePageInfo(pageNumber){
this.fetchOrders(pageNumber-1);
}

在methods中添加handleChangePageInfo处理函数,当用户点击分页上的按钮时,Vue会自动调用handleChangePageInfo来发起新的请求。

对了,还有最开始初始化的时候,我们要先发送一次API请求,来拉去首次的数据。我们可以在created阶段来调用。

created(){
this.fetchOrders(0);
}

到这里整个前后端的开发完毕,可以开始测试了!

完整的前端代码在public/index.html 文件中。

1.3.3 前端功能实现(JQuery+Bootstrap版)

img

如果需要了解传统的JQuery+Bootstrap版本的分页功能的小伙伴,可以继续往下看。当然这里我们的后端依然使用同一套代码。在public目录下创建一个jquery目录。

首先引入jQuery、Bootstrap和JsRender三个库。
<div id='app'>
<div class='example'>
<a class='btn btn-default btn-jump' href='/'>前往Vue+Element版——分页实现</a>
<table class='table table-bordered table-hover'>
<thead>
<tr>
<th>ID</th>
<th>名称</th>
</tr>
</thead>
<tbody class='table-body'>
</tbody>
</table>
<div class='pagination-container'>
<ul class='pagination pagination-lg'>
</ul>
</div>
</div>
</div>
按照Bootstrap的文档,放置好table和pagination标签。

接下来我们就可以写JS代码,在public/jquery目录下创建一个index.js文件。并把这个index.js引入到public/jquery/index.html文件中。

在jQuery项目中,我们需要需要自己维护所有的数据,更新逻辑,交互。

这里,我们先定义数据
const pageSize = 5;
let total = 0;
let currentPage = 0;
function getTotalPageCount(){
return Math.ceil(total / pageSize);
}
封装和后台交互的API

这里我们使用jQuery库自己的$.get来封装ajax请求,获得数据后把total,currentPage数据更新,然后主动调用updateUI去更新界面

这里就没有Vue,React这种数据驱动的框架那么舒服了,所有的事情都要自己去完成。

function getOrderInfo(pageNumber){
$.get( "/orders/"+pageSize+"/"+pageNumber, function( res ) {
console.log("获得的分页数据:",res);
total = res.data.total;
currentPage = res.data.currentPage;
updateUI(res.data);
});
}
定义界面更新逻辑
function updateUI(data){
//刷新Table的Body区域
updateTableUI(data);
//刷新底部分页
updatePagination(data);
}
function updateTableUI(data){
var template = $.templates("#orderTableBodyTmpl");
var htmlOutput = template.render( {
orders:data.orders,
} );
$( ".table-body" ).html( htmlOutput );
}
function updatePagination(data){
var template = $.templates("#paginationTmpl");
var pages = [];
for(let i=0;i<getTotalPageCount();i++){
pages.push({
id: i,
active: currentPage == i,
});
}
var htmlOutput = template.render({
pages: pages
});
$( ".pagination" ).html( htmlOutput );
$( ".pagination" ).find("li").each(function(){
var $this = $(this);
var pageNumber = Number($this.find("a").text());
console.log(pageNumber)
$this.click(function(){
getOrderInfo(pageNumber-1);
})
})
}

主更新逻辑updateUI内部分别封装了Table和分页的更新逻辑。

分页的更新逻辑里需要给每个分页按钮绑定事件,这里简单为每个按钮绑定了click事件,点击触发请求服务器数据的API getOrderInfo

到此,整个jQuery+Bootstrap的分页功能也实现了。

完整Demo源码-点击查看

下一期,我们来讲如何封装可复用的分页组件。