手把手教你使用Vue/React/Angular三大框架开发Pagination分页组件(中)丨【WEB前端大作战】

举报
Kagol 发表于 2021/04/20 21:17:43 2021/04/20
【摘要】 DevUI是一支兼具设计视角和工程视角的团队,服务于华为云DevCloud平台和华为内部数个中后台系统,服务于设计师和前端工程师。官方网站:devui.designNg组件库:ng-devui(欢迎Star)上一篇我们介绍了分页组件的模块设计,并用三大框架实现了一个不带实际分页功能的壳子组件,这次我们来一步步实现基本分页功能:只包含上一页/下一页。5 基本分页功能接下来我们开始给Pagina...

 

DevUI是一支兼具设计视角和工程视角的团队,服务于华为云DevCloud平台和华为内部数个中后台系统,服务于设计师和前端工程师。
官方网站:devui.design
Ng组件库:ng-devui(欢迎Star)

 

 

Kagol.png

 

 

上一篇我们介绍了分页组件的模块设计,并用三大框架实现了一个不带实际分页功能的壳子组件,这次我们来一步步实现基本分页功能:只包含上一页/下一页。

 

5 基本分页功能

接下来我们开始给Pagination组件添加实际的分页功能。

添加分页功能之前,我们先设计好Pagination组件的API:

  1. 数据总数 - total
  2. 每页数据数 - defaultPageSize
  3. 当前页码 - defaultCurrent
  4. 页码改变事件 - onChange

total和defaultPageSize两个参数可以合并为一个参数totalPage(总页码),不过考虑到后续的可扩展性(比如需要改变pageSize),将其拆分开来。

实现分页按钮分以下步骤:

  1. 实现一个通用的按钮组件
  2. 在分页组件中使用按钮组件
  3. 使用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>

这里要特别注意的是:

  1. Vue组件向外暴露事件的方式:使用$emit方法;
  2. 还有就是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)">&lt;</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实体字符&lt;代替。

之前设计的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)">&lt;</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" }
  ]
]

最终实现的分页效果如下:

现在做一个小小的总结,为了实现分页功能,我们:

  1. 先实现了一个通用的按钮组件;
  2. 然后使用这个通用组件,在Pagination组件中增加上一页/下一页两个翻页按钮,点击可以改变当前页码current;
  3. 接着使用做好的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);
      }}>&lt;</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);
}}>&lt;</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的差别是很明显的:

  1. 一是绑定事件的语法不同;
  2. 二是定义插槽的方式不同;
  3. 三是暴露外部事件和发射外部事件的方式不同。

这里也简单做一个对比:

 

 

绑定事件

定义插槽

外部事件

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)"
  >&lt;</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的值,不再赘言。

只是有一些差异需要注意⚠️:

  1. Angular的初始化方法是ngOnInit,Vue是created;
  2. 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

 

《在瀑布下用火焰烤饼:三步法助你快速定位网站性能问题》

《从零搭建一个灰度发布环境》

 
【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。