手把手教你使用Vue/React/Angular三大框架开发Pagination分页组件(中)丨【WEB前端大作战】
DevUI是一支兼具设计视角和工程视角的团队,服务于华为云DevCloud平台和华为内部数个中后台系统,服务于设计师和前端工程师。
官方网站:devui.design
Ng组件库:ng-devui(欢迎Star)
上一篇我们介绍了分页组件的模块设计,并用三大框架实现了一个不带实际分页功能的壳子组件,这次我们来一步步实现基本分页功能:只包含上一页/下一页。
5 基本分页功能
接下来我们开始给Pagination组件添加实际的分页功能。
添加分页功能之前,我们先设计好Pagination组件的API:
- 数据总数 - total
- 每页数据数 - defaultPageSize
- 当前页码 - defaultCurrent
- 页码改变事件 - onChange
total和defaultPageSize两个参数可以合并为一个参数totalPage(总页码),不过考虑到后续的可扩展性(比如需要改变pageSize),将其拆分开来。
实现分页按钮分以下步骤:
- 实现一个通用的按钮组件
- 在分页组件中使用按钮组件
- 使用Pagination组件对List进行分页
5.1 Vue版本
5.1.1 实现通用的按钮组件
通过前面编写的空的Pagination组件和List组件,相信大家对Vue组件都很熟悉了。
新建一个Button.vue组件文件,编写以下代码:
<template>
<button type="button" @click="$emit('click')"><slot></slot></button>
</template>
<script>
export default {
name: 'Button',
};
</script>
这里要特别注意的是:
- Vue组件向外暴露事件的方式:使用$emit方法;
- 还有就是Vue定义插槽的方式是使用<slot>标签。
其实以上的写法是一种简写形式,实际应该是这样:
<template>
<button type="button" @click="click()"><slot></slot></button>
</template>
<script>
export default {
name: 'Button',
methods: {
click() {
this.$emit('click');
}
},
};
</script>
$emit是Vue组件实例的是一个方法,用于组件对外暴露事件和传递数据,后面会看到传参的例子。
5.1.2 在Pagination组件中使用Button组件
做了这么多准备工作,终于可以做些实际的功能。
还记得之前我们编写了一个空的Pagination组件吗?这时我们可以往里面写点功能了。
<template>
<div class="x-pagination">
<Button class="btn-prev" @click="setPage(current - 1)"><</Button>
{{ current }}
<Button class="btn-next" @click="setPage(current + 1)">></Button>
</div>
</template>
<script>
import Button from './Button.vue';
export default {
name: 'Pagination',
components: {
Button,
},
// 接口定义 props
props: {
defaultCurrent: Number,
defaultPageSize: Number,
total: Number,
},
// 组件内部状态 data
data() {
return {
current: this.defaultCurrent,
}
},
// 计算属性
computed: {
totalPage: function () {
return Math.ceil(this.total / this.defaultPageSize);
},
},
// 内部方法定义
methods: {
setPage(page) {
if (page < 1) return;
if (page > this.totalPage) return;
this.current = page;
this.$emit('change', this.current);
},
}
};
</script>
将之前的文字“Pagination组件”删掉,加上上一页(<)/下一页(>)两个翻页按钮,另外我们也将当前页码current展示在两个翻页按钮中间,这样我们能更清楚当前处于第几页。
由于左尖括号与HTML标签的左尖括号冲突,不能直接使用,需要使用HTML实体字符<代替。
之前设计的Pagination组件的API参数都放到props里面:
// 接口定义 props
props: {
defaultCurrent: Number, // 默认当前页码
defaultPageSize: Number, // 默认每页数据数
total: Number, // 数据总数
}
我们定义了一个组件内部属性current,用于存放动态的页码:
// 组件内部状态 data
data() {
return {
current: this.defaultCurrent,
}
}
需要注意⚠️的是,data属性使用的是函数形式,在函数内部返回一个对象,current定义在该对象里面,这样可以确保每个实例可以维护一份被返回对象的独立的拷贝,具体原因可以参考官网的解释。
另外我们还定义了一个计算属性,用于获取总页码totalPage(限制页码边界时需要用到):
// 计算属性
computed: {
totalPage: function () {
return Math.ceil(this.total / this.defaultPageSize);
},
}
最后定义了一个内部方法setPage,用于改变页码:
// 内部方法定义
methods: {
setPage(page) {
if (page < 1) return; // 限制上一页翻页按钮的边界
if (page > this.totalPage) return; // 限制下一页翻页按钮的边界
this.current = page;
this.$emit('change', this.current);
},
}
当点击上一页/下一页翻页按钮时都会调用该方法,传入改变后的页码值。
如果是上一页,则传入current - 1:
<Button class="btn-prev" @click="setPage(current - 1)"><</Button>
下一页则是current + 1:
<Button class="btn-next" @click="setPage(current + 1)">></Button>
setPage中除了设置当前页码之外,还将页码改变事件发射出去,并将当前页码传到组件外部。
this.$emit('change', this.current);
另外也增加了一些限制翻页边界的逻辑,避免翻页时超过页码的边界,导致不必要的Bug:
if (page < 1) return; // 限制上一页翻页按钮的边界
if (page > this.totalPage) return; // 限制下一页翻页按钮的边界
5.1.3 使用Pagination组件对List进行分页
有了Pagination组件和List组件,就可以使用Pagination对List进行分页展示。
在Home.vue组件中使用Pagination组件。
<template>
<div class="home">
<img alt="Vue logo" src="../assets/logo.png">
<List :data-source="dataList" />
<Pagination :default-current="defaultCurrent" :default-page-size="defaultPageSize" :total="total" @change="onChange" />
</div>
</template>
<script>
import Pagination from '@/components/pagination/Pagination.vue';
import List from './List.vue';
import { lists } from '@/db';
import { chunk } from '@/util';
export default {
name: 'home',
components: {
Pagination,
List,
},
data() {
return {
defaultCurrent: 1,
defaultPageSize: 3,
total: lists.length,
dataList: [],
}
},
created() {
this.setList(this.defaultCurrent, this.defaultPageSize);
},
methods: {
onChange(current) {
this.setList(current, this.defaultPageSize);
},
setList: function(current, pageSize) {
this.dataList = chunk(lists, pageSize)[current - 1];
}
}
};
</script>
除了defaultCurrent/defaultPageSize/total这3个Pagination组件的参数外,我们在data内部状态中还定义了一个dataList字段,用于动态传入给List组件,达到分页的效果。
在setList方法中将对lists进行分块,并根据当前的页码获取分页数据,并赋值给dataList字段,这样List组件中就会展示相应的分页数据。
setList: function(current, pageSize) {
this.dataList = chunk(lists, pageSize)[current - 1];
}
setList方法在两处进行调用:created生命周期方法和onChange页码改变事件。
created生命周期事件在Vue实例初始化之后,挂载到DOM之前执行,在created事件中我们将第1页的数据赋值给dataList:
created() {
this.setList(this.defaultCurrent, this.defaultPageSize);
}
因此List组件将展示第1页的数据:
onChange事件是Pagination组件的页码改变事件,当点击上一个/下一页翻页按钮时执行,在该事件中可获取到当前的页码current。
我们在该事件中将当前页码的数据赋值给dataList,这样List组件将展示当前页码的数据,从而达到分页效果。
onChange(current) {
this.setList(current, this.defaultPageSize);
}
setList方法调用了chunk方法(作用与Lodash中的chunk方法类似),该方法用于将一个数组分割成指定大小的多个小数组,它的源码如下:
// 将数组按指定大小分块
export function chunk(arr = [], size = 1) {
if (arr.length === 0) return [];
return arr.reduce((total, currentValue) => {
if (total[total.length - 1].length === size) {
total.push([currentValue]);
} else {
total[total.length - 1].push(currentValue);
}
return total;
}, [[]]);
}
比如之前的lists数组,如果按每页3条数据进行分块chunk(lists, 3),则得到的结果如下:
[
[
{ "id": 1, "name": "Curtis" },
{ "id": 2, "name": "Cutler" },
{ "id": 3, "name": "Cynthia" }
],
[
{ "id": 4, "name": "Cyril" },
{ "id": 5, "name": "Cyrus" },
{ "id": 6, "name": "Dagmar" }
],
[
{ "id": 7, "name": "Dahl" },
{ "id": 8, "name": "Dahlia" },
{ "id": 9, "name": "Dailey" }
],
[
{ "id": 10, "name": "Daine" }
]
]
最终实现的分页效果如下:
现在做一个小小的总结,为了实现分页功能,我们:
- 先实现了一个通用的按钮组件;
- 然后使用这个通用组件,在Pagination组件中增加上一页/下一页两个翻页按钮,点击可以改变当前页码current;
- 接着使用做好的Pagination组件对List列表组件进行分页。
接下来我们看下React如何实现以上功能。
5.2 React版本
5.1.1 实现通用的按钮组件
同样也是先定义一个通用按钮组件Button.js:
import React from 'react';
function Button({ onClick, children }) {
return (
<button type="button" onClick={ onClick }>{ children }</button>
);
}
export default Button
通过前面开发的Pagination/List组件,相信大家对React的函数组件并不陌生了。
和Vue不同的是,React不需要对外发射事件之类的操作,传什么事件进来直接就发射出去了;
另一个不同是定义插槽的方式,React使用props.children代表组件标签中间传入的内容。
5.1.2 在Pagination组件中使用Button组件
然后使用通用按钮组件,在Pagination组件中增加上一页/下一页两个翻页按钮:
import React, { useState } from 'react';
import Button from './Button';
function Pagination(props) {
const { total, defaultCurrent, defaultPageSize, onChange } = props;
// 声明一个叫 “current” 的 state 变量,用来保存当前的页码;
// setPage方法是用来改变current的。
const [current, setPage] = useState(defaultCurrent);
const totalPage = Math.ceil(total / defaultPageSize);
return (
<div className="m-pagination">
<Button className="btn-prev" onClick={() => {
if (current < 2) return;
setPage(current - 1);
onChange(current - 1);
}}><</Button>
{{ current }}
<Button className="btn-next" onClick={() => {
if (current >= totalPage) return;
setPage(current + 1);
onChange(current + 1);
}}>></Button>
</div>
);
}
export default Pagination;
这里引出React 16.8之后一个很重要的概念:React Hooks。
为了在函数组件中定义组件内部状态,从react库中引入了useState这个方法:
import React, { useState } from 'react';
useState就是一个Hook,通过在函数组件里调用它来给组件添加一些内部state,React会在重复渲染时保留这个state。
useState会返回一对值:当前状态和一个让你更新它的函数。
useState唯一的参数就是初始state,这里是默认当前页码(defaultCurrent),这个初始 state 参数只有在第一次渲染时会被用到。
const [current, setPage] = useState(defaultCurrent);
当点击上一页/下一页翻页按钮时,我们调用了setPage方法,传入新的页码,从而改变current当前页码,实现分页功能。
另外也和Vue版本一样,通过调用onChange方法将页码改变事件发射出去,并将当前页码传递到组件之外。
如果是上一页:
<Button className="btn-prev" onClick={() => {
if (current < 2) return;
setPage(current - 1);
onChange(current - 1);
}}><</Button>
如果是下一页:
<Button className="btn-next" onClick={() => {
if (current >= totalPage) return;
setPage(current + 1);
onChange(current + 1);
}}>></Button>
5.1.3 使用Pagination组件对List进行分页
Pagination组件做好了,我们就可以使用它来给List列表组件进行分页啦。
在App.js中引入List和Pagination组件:
import React, { useState } from 'react';
import Pagination from './components/pagination/Pagination';
import List from './components/List';
import { lists } from './db';
import { chunk } from './util';
import './App.scss';
function App() {
const defaultCurrent = 1;
const defaultPageSize = 3;
// 设置List默认分页数据:第一页的数据chunk(lists, defaultPageSize)[defaultCurrent - 1]
const [dataSource, setLists] = useState(chunk(lists, defaultPageSize)[defaultCurrent - 1]);
return (
<div className="App">
<List dataSource={dataSource} />
<Pagination total={lists.length} defaultCurrent={defaultCurrent} defaultPageSize={defaultPageSize} onChange={current => {
// 页码改变时,重新设置当前的分页数据
setLists(chunk(lists, defaultPageSize)[current - 1]);
}} />
</div>
);
}
export default App;
同样也是定义了一个List组件的数据源(使用useState这个React Hook):dataSource,默认设置为第一页的数据:
// 设置List默认分页数据:第一页的数据chunk(lists, defaultPageSize)[defaultCurrent - 1]
const [dataSource, setLists] = useState(chunk(lists, defaultPageSize)[defaultCurrent - 1]);
当页码改变时,Pagination的onChange事件能捕获到并执行,该事件中可以拿到当前页码current,这时我们可以通过调用useState的第2个返回值——setLists方法——来改变dataSource数据源,实现分页功能:
<Pagination ... onChange={current => {
// 页码改变时,重新设置当前的分页数据
setLists(chunk(lists, defaultPageSize)[current - 1]);
}} />
在组件内维护状态的方式,React和Vue相差较大,这里做一个简单的对比:
|
组件内部状态存放位置 |
改变组件内部状态的方式 |
React |
useState第1个返回值。 const [state, setState] = useState(initialState]; |
useState第2个返回值(一个方法)。 const [state, setState] = useState(initialState]; |
Vue |
data方法中。 data() { return { state: [], } } |
methods对象中。 methods: { setState: function() { // 执行具体的代码 } } |
另外还有一个需要注意⚠️:
在Vue中,为了初始化List的数据源,没法直接在data中写,比如:
data() {
return {
dataList: chunk(lists, this.defaultPageSize)[this.defaultCurrent - 1],
}
}
而是必须在created初始化方法中写:
created() {
this.dataList = chunk(lists, this.defaultPageSize)[this.defaultCurrent - 1];
}
而在React中则显得简洁和自然许多:
// 设置List默认分页数据:第一页的数据
const [dataSource, setLists] = useState(chunk(lists, defaultPageSize)[defaultCurrent - 1];
不过React这种写法对初学者是不友好的,习惯之后会觉得很舒服。
5.3 Angular版本
5.1.1 实现通用的按钮组件
最后来看下Angular如何实现分页功能,思路都一样,先定义一个通用按钮组件button.component.ts:
import { Component, Output, EventEmitter } from "@angular/core";
@Component({
selector: 'x-button',
template: `
<button type="button" (click)="onClick()"><ng-content></ng-content></button>
`,
})
export class ButtonComponent {
@Output() btnClick = new EventEmitter();
onClick() {
this.btnClick.emit();
}
}
Angular和React/Vue的差别是很明显的:
- 一是绑定事件的语法不同;
- 二是定义插槽的方式不同;
- 三是暴露外部事件和发射外部事件的方式不同。
这里也简单做一个对比:
|
绑定事件 |
定义插槽 |
外部事件 |
Vue |
v-on指令(简写形式:@) |
<slot>标签 |
$emit() |
React |
props传递 props.onClick |
props.children |
props传递,无需发射 |
Angular |
括号符() (click)="btnClick()" |
<ng-content>标签 |
@Output()+emit() |
5.1.2 在Pagination组件中使用Button组件
现在模板中使用通用按钮组件pagination.component.html:
<div class="x-pagination">
<x-button
class="btn-prev"
(btnClick)="setPage(current - 1)"
><</x-button>
{{ current }}
<x-button
class="btn-next"
(btnClick)="setPage(current + 1)"
>></x-button>
</div>
然后在pagination.component.ts中定义具体逻辑:
import { Component, Input, Output, EventEmitter } from "@angular/core";
@Component({
selector: 'x-pagination',
templateUrl: './pagination.component.html',
styleUrls: ['./pagination.component.scss']
})
export class PaginationComponent {
// 组件接口定义
@Input() total: number;
@Input() defaultCurrent = 1;
@Input() defaultPageSize: number;
@Output() onChange = new EventEmitter();
// 计算属性
@Input()
get totalPage() {
return Math.ceil(this.total / this.defaultPageSize);
}
// 组件内部状态
current = this.defaultCurrent;
// 组件方法
setPage(page) {
if (this.current < 2) return;
if (this.current > this.totalPage - 1) return;
this.current = page;
this.onChange.emit(this.current);
}
}
和Vue/React一样,定义组件接口/计算属性/内部状态/组件方法,只是具体的语法不同,语法上的对比前面已经说明,不再赘言。
下面直接介绍如何使用Pagination组件对List进行分页。
5.1.3 使用Pagination组件对List进行分页
在app.component.html中引入Pagination/List两个组件:
<x-list [dataSource]="dataSource"></x-list>
<x-pagination
[total]="total"
[defaultCurrent]="defaultCurrent"
[defaultPageSize]="pageSize"
(onChange)="onChange($event)"
></x-pagination>
在app.component.ts中定义具体逻辑:
import { Component, OnInit } from '@angular/core';
import { lists } from './db';
import { chunk } from './util';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
defaultCurrent = 1;
defaultPageSize = 3;
total = lists.length;
dataSource = [];
ngOnInit() {
this.setLists(this.defaultCurrent, this.defaultPageSize);
}
onChange(current) { // 页码改变
this.setLists(current, this.defaultPageSize);
}
setLists(page, pageSize) {
this.dataSource = chunk(lists, pageSize)[page - 1];
}
}
思路也是一样的,定义一个List组件的数据源dataSource,组件初始化(ngOnInit)时给dataSource设置初始分页数据(第一页数据),然后在页码改变时重新设置dataSource的值,不再赘言。
只是有一些差异需要注意⚠️:
- Angular的初始化方法是ngOnInit,Vue是created;
- Angular绑定属性的方式是使用中括号[],Vue是使用v-bind指令(或者简写方式:key)。
至此三大框架实现基本分页功能的方法及其差异都已介绍完毕,下一篇将继续实现分页组件最核心的内容:分页器的实现。
尽情期待!
附上3大框架的Pagination组件源码地址:
https://github.com/kagol/components
本文参考DevUI分页组件写成,该组件源码地址:
https://github.com/DevCloudFE/ng-devui/tree/master/devui/pagination
欢迎大家关注DevUI组件库,给我们提意见和建议,也欢迎Star。
.
手把手教你使用Vue/React/Angular三大框架开发Pagination分页组件系列文章:
手把手教你使用Vue/React/Angular三大框架开发Pagination分页组件(上)丨【WEB前端大作战】
手把手教你使用Vue/React/Angular三大框架开发Pagination分页组件(下)丨【WEB前端大作战】
.
加入我们
我们是DevUI团队,欢迎来这里和我们一起打造优雅高效的人机设计/研发体系。招聘邮箱:muyang2@huawei.com。
文/DevUI Kagol
- 点赞
- 收藏
- 关注作者
评论(0)