DevUI是一支兼具设计视角和工程视角的团队,服务于华为云DevCloud平台和华为内部数个中后台系统,服务于设计师和前端工程师。
官方网站:devui.design
Ng组件库:ng-devui(欢迎Star)css
“他在正午、黄昏,在一天里的许多时刻去感觉它、记录它,结果也就让咱们看到了那么多的不一样。他描绘它的角度没变,但它的面目却极大地改变了。”html
19世纪著名的印象派画家莫奈,喜欢对着同一处景物,分别画出对象在不一样时间,不一样光线下的色彩变化。前端
好比不一样季节的三株白杨:vue
好比一天中不一样时刻的浮翁大教堂:node
若是同一个组件,用不一样的框架实现,会有什么不一样呢?react
带着这个想法,我分别选用目前最火的Vue/React/Angular三大框架,去实现一个简单的Pagination分页组件。git
咱们要实现的分页组件大体效果以下:github
主要包含如下功能:vue-cli
从设计稿能够看出,Pagination组件主要由2个模块组成:npm
咱们采用自上而下的方式建立组件,先建立一个空的Pagination组件。
注意⚠️
我使用的框架版本号以下:
node@10.15.1
vue-cli@3.7.0
vue@2.6.10
create-react-app@3.0.1
react@16.8.6
angular-cli@7.3.9
angular@7.2.0
使用Vue CLI建立一个基础Vue项目,并输入npm run serve命令启动起来。
而后在components文件夹新建一个pagination文件夹,里面新建咱们须要的3个组件文件:
在Pagination.vue文件中增长如下代码:
<template> <div class="x-pagination"> Pagination组件 </div> </template> <script> export default { name: 'Pagination', }; </script>
Vue组件的特色是将HTML/CSS/JavaScript都统一放在一个.vue后缀的文件中。
对于习惯将HTML/CSS/JavaScript分开编写的前端开发者来讲,显得很是天然,加上Vue的语法很是简洁,入门门槛比较低,因此2014年一经推出,很快便席卷全球。
在views/Home.vue中使用Pagination组件:
<template> <div class="home"> <img alt="Vue logo" src="../assets/logo.png"> <HelloWorld msg="Welcome to Your Vue.js App by kagol"/> <Pagination /> </div> </template> <script> // @ is an alias to /src import HelloWorld from '@/components/HelloWorld.vue'; import Pagination from '@/components/pagination/Pagination.vue'; export default { name: 'home', components: { HelloWorld, Pagination, }, }; </script>
组件的使用方式也和普通HTML元素很相似:
<Pagination />
须要注意的是使用Vue局部组件以前须要在components中声明该组件。
这只是一个空组件,只显示了“Pagination组件”文字,没有太大的意义,不过不要着急,后面咱们会一步步完善该组件,实现咱们想要的功能,并能不断扩展和演进。在继续开发Vue版本的Pagination组件以前,咱们先来看看其余框架如何实现和使用一个组件。
如下是显示效果:
先来看看React框架,咱们一样使用Create React App建立一个基础的React项目,并输入命令npm start命令启动。
和Vue项目同样,建立如下3个组件文件:
在Pagination.js文件中增长如下代码:
import React from 'react'; function Pagination() { return ( <div className="x-pagination"> Pagination组件 </div> ); } export default Pagination;
能够看到React开发组件的方式和Vue相差很是大,React推崇函数式编程(FP,Functional Programming),每一个React组件都是一个函数,HTML/CSS/JavaScript都在函数里面,在函数里面返回模板内容。
须要注意⚠️的是在React中HTML元素的class须要写成className,缘由是class是JavaScript中的保留关键字,而React使用的JSX是JavaScript的扩展,使用class会致使命名冲突。
React这种写法很特别,初学者可能会不太习惯,不过一旦用习惯了,会以为很是爽,以为一切都很是合理,组件就应该这样写。
在App.js中使用Pagination组件:
import React from 'react'; import Pagination from './components/pagination/Pagination'; import './App.scss'; function App() { return ( <div className="App"> <Pagination /> </div> ); } export default App;
使用React组件的方式也很简单,和使用普通HTML元素相似:
<Pagination />
显示的效果与Vue版本无异。
和Vue/React这种专一View视图层的轻量级框架不一样,Angular是一个很重的框架,配备很是完整,Web开发过程当中你须要的一切,Angular框架都给你提供好了,你只须要随手取用便可。
咱们一块儿来看看怎么开发一个Angular组件吧。
一样是使用Angular CLI建立一个基础的Angular项目,并输入命令npm start命令启动。
和React/Vue组件不一样,Angular组件不能单独使用,须要包一层Module,所以咱们须要建立1个模块文件和3个组件文件:
HTML/CSS能够放在ts文件里面,也能够放在单独的文件里。
通常而言,HTML/CSS内容较少时,会将它们放到ts文件里。
先建立Pagination模块,在pagination.module.ts文件中增长如下代码:
import { NgModule } from "@angular/core"; @NgModule() export class PaginationModule { }
而后是建立Pagination组件,在pagination.component.ts文件中增长如下代码:
import { Component } from "@angular/core"; @Component({ selector: 'x-pagination', template: ` <div class="x-pagination"> Pagination组件 </div> `, }) export class PaginationComponent { }
Angular和Vue/React很是明显的区别已经显示出来:
首先是组件须要依托于Module存在;
而后是无论是定义Module仍是Component都须要使用装饰器;
好比定义一个Angular模块须要使用@NgModule装饰器,定义一个Angular组件须要使用@Component装饰器。
还有就是Angular推崇的是面向对象的编程范式,Angular里面的几乎一切都是类和对象,除了刚才一经介绍的模块和组件,还有服务(Service)、管道(Pipe)等,都是类(class)。
为了使用Pagination组件,咱们须要先导入Pagination模块,并声明Pagination组件,在app.module.ts文件中增长如下代码:
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { AppComponent } from './app.component'; import { PaginationModule } from './components/pagination/pagination.module'; import { PaginationComponent } from './components/pagination/pagination.component'; @NgModule({ declarations: [ AppComponent, PaginationComponent, // 声明Pagination组件 ], imports: [ BrowserModule, PaginationModule, // 导入Pagination模块 ], providers: [], bootstrap: [AppComponent] }) export class AppModule
而后就能使用Pagination组件了,在app.component.ts文件中增长如下代码:
<div style="text-align:center"> <h1> Welcome to {{ title }}! </h1> <img width="300" alt="Angular Logo" src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTAgMjUwIj4KICAgIDxwYXRoIGZpbGw9IiNERDAwMzEiIGQ9Ik0xMjUgMzBMMzEuOSA2My4ybDE0LjIgMTIzLjFMMTI1IDIzMGw3OC45LTQzLjcgMTQuMi0xMjMuMXoiIC8+CiAgICA8cGF0aCBmaWxsPSIjQzMwMDJGIiBkPSJNMTI1IDMwdjIyLjItLjFWMjMwbDc4LjktNDMuNyAxNC4yLTEyMy4xTDEyNSAzMHoiIC8+CiAgICA8cGF0aCAgZmlsbD0iI0ZGRkZGRiIgZD0iTTEyNSA1Mi4xTDY2LjggMTgyLjZoMjEuN2wxMS43LTI5LjJoNDkuNGwxMS43IDI5LjJIMTgzTDEyNSA1Mi4xem0xNyA4My4zaC0zNGwxNy00MC45IDE3IDQwLjl6IiAvPgogIDwvc3ZnPg=="> </div> <x-pagination></x-pagination>
使用Angular组件的方式和普通的HTML元素相似:
<x-pagination></x-pagination>
显示的效果与Vue/React同样。
在添加实际的分页功能以前咱们须要先作一个List组件,用来模拟分页数据的展现。
根据咱们以前介绍的3个框架实现组件的方式,而后稍微增长些额外的知识,咱们就能很快作一个数据渲染组件List。
仍是先看Vue框架吧。
新建List.vue组件文件,输入如下代码:
<template> <ul> <li v-for="list in lists" :key="list.id"> {{ list.name }} </li> </ul> </template> <script> export default { name: 'List', props: { dataSource: Array }, data() { return { lists: this.dataSource } }, watch: { // 对dataSource进行监听,若是发生变化则从新将新值赋给lists dataSource: { handler(newValue, oldValue) { this.lists = newValue; } } } }; </script>
在template模板部分,咱们使用Vue的v-for指令,在li元素中循环lists数组,并将name值显示出来。其中的:key是v-bind:key的简写形式,为元素绑定惟一的key值,用于DOM对比时的性能优化。
1) 经过props传入数据
本来我打算直接将lists的值放到props中,经过外部传进来,以下:
<template> <ul> <li v-for="list in lists" :key="list.id"> {{ list.name }} </li> </ul> </template> <script> export default { name: 'List', props: { lists: Array } }; </script>
这样有一个问题,就是外部传入的lists若是发生变化,template里绑定的lists不会相应的变化。
2) 维护内部状态
为了监听props中的值的变化,我把lists放到组件内部状态中(data),外部传入的数据叫dataSource,以下:
<script> export default { name: 'List', props: { dataSource: Array }, data() { return { lists: this.dataSource } }, }; </script>
3) 监听外部props的变化
而后监听dataSource的变化,当dataSource变化时,将新值赋值给lists:
watch: { // 对dataSource进行监听,若是发生变化则从新将新值赋给lists dataSource: { handler(newValue, oldValue) { this.lists = newValue; } } }
传入List组件的lists数组以下:
export const lists = [ { 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' }, ];
使用List组件展现数据:
<List :data-source="lists" />
这里须要注意⚠️的是,全部绑定的数据须要使用短横线命名法,好比上面的data-source,对应data中驼峰命名法的dataSource。
展现的效果以下:
React编写的是函数组件,props的变化会直接反映到模板中,不须要单独监听,因此写起来很是简洁:
import React from 'react'; function List({ dataSource }) { return ( <ul className="m-list"> { dataSource.map(list => { return <li key={ list.id }>{ list.name }</li>; }) } </ul> ); } export default List
外部数据经过函数的props参数传入,这里将props进行了对象解构,直接取到了dataSource字段。
还有一点和Vue不太同样,就是React是函数式编程的写法,列表数据的渲染不须要v-for之类的指令,而是经过数组的map方法,直接返回相应的li元素便可,看着很是天然。其中li元素上绑定的key值与Vue中key值的做用相似。
使用方式和Vue的相似:
<List dataSource={dataSource} />
Angular稍微麻烦些,须要同时定义Module和Component:
先编写list.module.ts:
import { NgModule } from "@angular/core"; @NgModule() export class ListModule { }
而后编写List组件list.component.ts:
import { Component, Input } from "@angular/core"; @Component({ selector: 'x-list', template: ` <ul> <li *ngFor="let list of dataSource; trackBy: trackByIndex"> {{ list.name }} </li> </ul> `, }) export class ListComponent { @Input() dataSource; trackByIndex(index, list){ return list.id; } }
Angular和Vue/React的差异比较大:
Angular组件的使用方式,却是和其余框架大同小异:
<x-list [dataSource]="dataSource"></x-list>
接下来咱们开始给Pagination组件添加实际的分页功能。
添加分页功能以前,咱们先设计好Pagination组件的API:
total和defaultPageSize两个参数能够合并为一个参数totalPage(总页码),不过考虑到后续的可扩展性(好比须要改变pageSize),将其拆分开来。
实现分页按钮分如下步骤:
经过前面编写的空的Pagination组件和List组件,相信你们对Vue组件都很熟悉了。
新建一个Button.vue组件文件,编写如下代码:
<template> <button type="button" @click="$emit('click')"><slot></slot></button> </template> <script> export default { name: 'Button', }; </script>
这里要特别注意的是:
其实以上的写法是一种简写形式,实际应该是这样:
<template> <button type="button" @click="click()"><slot></slot></button> </template> <script> export default { name: 'Button', methods: { click() { this.$emit('click'); } }, }; </script>
$emit是Vue组件实例的是一个方法,用于组件对外暴露事件和传递数据,后面会看到传参的例子。
作了这么多准备工做,终于能够作些实际的功能。
还记得以前咱们编写了一个空的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; // 限制下一页翻页按钮的边界
有了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" } ] ]
最终实现的分页效果以下:
如今作一个小小的总结,为了实现分页功能,咱们:
接下来咱们看下React如何实现以上功能。
一样也是先定义一个通用按钮组件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表明组件标签中间传入的内容。
而后使用通用按钮组件,在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>
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这种写法对初学者是不友好的,习惯以后会以为很舒服。
最后来看下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() |
如今模板中使用通用按钮组件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进行分页。
在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的值,再也不赘言。
只是有一些差别须要注意⚠️:
至此三大框架实现基本分页功能的方法及其差别都已介绍完毕,后一节将介绍本文最核心的内容:分页器的实现。
咱们再来回顾下分页组件的模块图:
中间显示页码的部分就是分页器,它的核心是页码显示和页码省略的逻辑。
为了方便地跳转到任意页码,却又不至于在页面中显示太多页码,页码并非始终所有显示出来的,而是在页码少时所有显示,页码多时只显示部分页码。这就存在显示策略问题。
咱们从当前页码出发,好比模块图中当前页码是第5页:
那么以该页码为中心,两边显示必定的页码,好比两边各显示2页;
另外首页和尾页须要始终显示出来,方便回到首页和跳转到尾页;
首页到第3页中间的页码以及第7页到尾尾的页码都隐藏起来,而且支持点击左/右更多按钮,快捷跳转多页(好比5页)的功能。
另外须要考虑页码少的状况,若是只有8页怎么显示呢?
很简单,直接去掉右边的更多按钮就好:
若是当前页码在第4页呢?去掉左边的更多按钮,显示右边的更多按钮便可:
以上就是所有的页码显示策略。
现简述以下:
接下来看看如何用三大框架实现这个逻辑。
编写Pager分页器组件以前,仍是设计好组件的API:
而后先写好模板,在Pager.vue的<template>中编写如下代码:
<template> <ul class="x-pager"> <li class="number">1</li> <li class="more left"></li> <li class="number"></li> <li class="more right"></li> <li class="number">{{ totalPage }}</li> </ul> </template>
再在<script>中写基本的逻辑:
<script> import Vue from 'vue'; export default { name: 'Pager', // 组件接口定义 props: { totalPage: Number, // 总页数 defaultCurrent: Number, // 默认当前页码 }, }; </script>
搭好基本框架以后,咱们采起最小可用产品(Minimum Viable Product,MVP)的思路:
分3步实现分页器功能:
先显示第1步:首页尾页的显示和跳页逻辑:
首页
<li class="number" :class="{ active: this.current == 1 }" @click="setPage(1)" >1</li>
尾页
<li class="number" :class="{ active: this.current == totalPage }" v-if="totalPage !== 1" @click="setPage(totalPage)" >{{ totalPage }}</li>
因为当前页码有可能从Pager组件外部改变(上一页/下一页按钮),由于须要监听defaultCurrent的变化,须要增长组件内部状态current代替defaultCurrent:
data() { return { current: this.defaultCurrent, // 当前页码 } }
而后监听defaultCurrent,当外部传入的defaultCurrent发生变化时,将新值赋值给current:
watch: { defaultCurrent: { handler(newValue, oldValue) { this.current = newValue; } } }
接着定义翻页方法:
methods: { setPage(page) { // 对页码进行限制,不能超出[1, totalPage]的范围 let newPage = page; if (page < 1) newPage = 1; if (page > this.totalPage) newPage = this.totalPage; this.current = newPage; // 设置当前页码 this.$emit('change', this.current); // 向外发射页码改变事件 } }
显示的效果以下:
咱们能够在Pagination组件中试试第一版的Pager。
在Pagination.vue中,去掉以前页码显示的那一行代码,使用Pager组件替代:
<template> <div class="m-pagination"> <Button class="btn-prev" @click="setPage(current - 1)"><</Button> // 去掉该行 {{ current }},替换成如下Pager组件 <Pager :total-page="totalPage" :default-current="current" @change="onChange"></Pager> <Button class="btn-next" @click="setPage(current + 1)">></Button> </div> </template>
而后增长Pager的onChange页码改变的回调事件:
methods: { onChange(current) { this.current = current; // 设置当前页码 this.$emit('change', this.current); // 向Pagination组件外发射页码改变事件 } }
能够试试首/尾页的翻页效果:
有了首尾页的翻页还不够,还须要继续完善更多按钮的快捷翻页功能。
先梳理下更多按钮的显示逻辑:
具体实现以下:
<!-- 左更多按钮 --> <li class="more left" v-if="totalPage > 7 && current >= 5" ></li> <!-- 右更多按钮 --> <li class="more right" v-if="totalPage > 7 && current <= totalPage - 4" ></li>
不过咱们不想写死这些数字,假设中间页码数为centerSize(这里是5),能够重构成:
<li class="more left" v-if="totalPage > centerSize + 2 && current >= centerSize" ></li> <li class="more right" v-if="totalPage > centerSize + 2 && current <= totalPage - centerSize + 1" ></li>
接着是增长快捷翻页事件:
<li class="more left" v-if="totalPage > centerSize + 2 && current >= centerSize" @click="setPage(current - jumpSize)" ></li> <li class="more right" v-if="totalPage > centerSize + 2 && current <= totalPage - centerSize + 1" @click="setPage(current - jumpSize)" ></li>
注意⚠️:为了避免写死每次快捷跳转的页码,咱们用jumpSize保存该值。
接下来咱们能够看看快捷翻页的效果,为了清楚看出当前处于哪一页,咱们暂时将中间为哦未实现的页码按钮组显示成当前页码:
<!-- 中间页码组 --> <li class="number">{{ current }}</li>
初始在第1页:
点击右更多按钮以后(跳转到第6页):
再点击右更多按钮(跳转到第11页):
点击左更多按钮则又回到第6页,完美达到预期。
中间页码组centerPages是一个长度在[0, centerSize]之间的数组,它的值由总页码totalPage和当前页码current共同决定,计算规则以下:
将centerPages定义为计算属性,具体实现以下:
computed: { centerPages: function() { // 中间页码计算 let centerPage = this.current; if (this.current > this.totalPage - 3) { centerPage = this.totalPage - 3; } if (this.current < 4) { centerPage = 4; } if (this.totalPage <= this.centerSize + 2) { // 总页码较小时,所有显示出来 const centerArr = []; for (let i = 2; i < this.totalPage; i++) { centerArr.push(i); } return centerArr; } else { // 总页码较大时,只显示中间centerSize个页码 const centerArr = []; for (let i = centerPage - 2; i <= centerPage + 2; i++) { centerArr.push(i); } return centerArr; } }
有了中间页码数组,就能够渲染中间页码组:
<!-- 中间页码组 --> <li class="number" v-for="(page, index) in centerPages" :key="index" >{{ page }}</li>
接着为其增长active类(用于高亮)和绑定点击事件(用于跳转到相应的页码):
<!-- 中间页码组 --> <li class="number" :class="{ active: current === page }" v-for="(page, index) in centerPages" :key="index" @click="setPage(page)" >{{ page }}</li>
最终效果以下:
只有1页的状况:
<=7页的状况:
>7页且当前页码<=4页的状况:
>7页且当前页码>4页的状况:
至此,Vue版本分页器组件已所有实现,整个Pagination组件也所有实现。
接下来看看React/Angular如何实现分页器吧。
一样采MVP的思路,咱们按如下步骤开发Pager分页器组件:
咱们先搭建基本模板框架,在Pager.js中编写如下代码:
import React from 'react'; function Pager({ totalPage, defaultCurrent, onChange }) { return ( <ul className="x-pager"> <li className="number">1</li> <li className="more left"></li> <li className="number"></li> <li className="more right"></li> <li className="number">{ totalPage }</li> </ul> ); } export default Pager;
这只是一个空壳子,什么都作不了,接下来咱们加点实际的功能。
增长首尾页显示条件、高亮条件和翻页功能。
import React, { useState } from 'react'; function Pager({ totalPage, defaultCurrent, onChange }) { // 使用useState定义内部状态:当前页码current const [current, setPage] = useState(defaultCurrent); return ( <ul className="x-pager"> <li className={'number' + (current == 1 ? ' active' : '')} onClick={() => { setPage(1); onChange(1); }} >1</li> <li className="more left"></li> <li className="number"></li> <li className="more right"></li> { totalPage !== 1 && <li className={'number' + (current == totalPage ? ' active' : '')} onClick={() => { setPage(totalPage); onChange(totalPage); }} >{ totalPage }</li> } </ul> ); } export default Pager;
值得注意的是条件渲染的写法,React和Vue仍是有点区别的:
另外就是Vue中有标签class绑定的功能,而React没有相似的功能,须要经过在{}大括号中写三目运算符来判断高亮。
至此Pager已经能够直接拿去Pagination中使用了,不过只能首页和尾页翻页,接下来继续加强Pager的功能。
更多按钮显示的逻辑和Vue版本同样:
左更多按钮:
const centerSize = 5; // 中间按钮组的页码数 const jumpSize = 5; // 快捷翻页的页数 { totalPage > centerSize + 2 && current >= centerSize && <li className="more left" onClick={() => { setPage(current - jumpSize); // 设置快捷翻页后的新页码 onChange(current - jumpSize); // 页码改变时的外部回调事件 }} ></li> }
右更多按钮:
{ totalPage > centerSize + 2 && current <= totalPage - centerSize + 1 && <li className="more right" onClick={() => { setPage(current + jumpSize); onChange(current + jumpSize); }} ></li> }
最后实现页码按钮组功能。
主要是须要计算好centerPages页码数组,计算逻辑和Vue的同样:
先计算centerPages:
// 计算中间页码数组 const centerPages = []; let centerPage = current; if (current > totalPage - 3) { centerPage = totalPage - 3; } if (current < 4) { centerPage = 4; } if (totalPage <= centerSize + 2) { for (let i = 2; i < totalPage; i++) { centerPages.push(i); } } else { for (let i = centerPage - 2; i <= centerPage + 2; i++) { centerPages.push(i); } }
而后将其显示出来:
{ centerPages.map((page, index) => { return ( <li key={index} className={'number' + (page == current ? ' active' : '')} onClick={() => { setPage(page); onChange(page); }} >{ page }</li> ); }) }
列表渲染的方式须要注意⚠️:
因为Pager中的当前页码有可能经过外部改变(好比上一页/下一页按钮),由于在传入的defaultCurrent变化时,须要动态改变current,这须要借助另外一个React Hook——useEffect——来实现,具体代码以下:
// 外部传入的defaultCurrent变化时,须要从新设置current useEffect(() => { setPage(defaultCurrent); });
另外须要注意的就是更多按钮快捷翻页可能会越界,须要加以显示,为此咱们编写了一个limitPage方法:
const limitPage = (page) => { if (page < 1) return 1; if (page > totalPage) return totalPage; return page; }
在更多按钮的事件中使用:
左更多按钮:
{ totalPage > centerSize + 2 && current >= centerSize && <li className="more left" onClick={() => { setPage(limitPage(current - jumpSize)); // 设置快捷翻页后的新页码 onChange(limitPage(current - jumpSize)); // 页码改变时的外部回调事件 }} ></li> }
右更多按钮:
{ totalPage > centerSize + 2 && current <= totalPage - centerSize + 1 && <li className="more right" onClick={() => { setPage(limitPage(current + jumpSize)); onChange(limitPage(current + jumpSize)); }} ></li> }
这样就完成了React版本的Pager分页器组件,除了细微语法上的差别外,大部分代码逻辑都是同样的。
接下来即将介绍的Angular版本的Pager也是同样的,大部分逻辑均可以复用。
Angular实现Pager的思路和Vue/React也差很少,就是写法上的差别,一样按MVP的思路,分红如下3个步骤:
先实现首/尾页翻页功能。
先作模板,在pager.component.html中编写如下代码:
<ul class="x-pager"> <li [ngClass]="{ number: true, active: 1 == current }" (click)="setPage($event, 1)" >1</li> <li class="more left"></li> <li class="number" ></li> <li class="more right"></li> <li *ngIf="totalPage !== 1" [ngClass]="{ number: true, active: totalPage == current }" (click)="setPage($event, totalPage)" >{{ totalPage }}</li> </ul>
而后在pager.component.ts中写具体逻辑:
import { Component, Input, Output, EventEmitter } from "@angular/core"; @Component({ selector: 'x-pager', templateUrl: './pager.component.html', styleUrls: ['./pager.component.scss'] }) export class PagerComponent { @Input() totalPage: number; @Input() defaultCurrent: number; @Output() onChange = new EventEmitter(); current = this.defaultCurrent; setPage($event, page) { this.current = page; this.onChange.emit(this.current); } }
因为用于设置页码的方法setPage前面已经写好了,所以只须要在模板中新加左/右更多按钮便可:
<li class="more left" *ngIf="totalPage > centerSize + 2 && current >= centerSize" (click)="setPage($event, current - centerSize)" ></li> <li class="more right" *ngIf="totalPage > centerSize + 2 && current <= totalPage - centerSize + 1" (click)="setPage($event, current + centerSize)" ></li>
最后是实现页码按钮组,关键仍是centerPages数组的计算,计算逻辑能够复用Vue/React的。具体实现以下:
@Input() get centerPages() { let centerPage = this.current; if (this.current > this.totalPage - 3) { centerPage = this.totalPage - 3; } if (this.current < 4) { centerPage = 4; } const centerArr = []; if (this.totalPage < this.centerSize + 2) { for (let i = 2; i < this.totalPage; i++) { centerArr.push(i); } } else { for (let i = centerPage - 2; i <= centerPage + 2; i++) { centerArr.push(i); } } return centerArr; }
相似Vue中的计算属性(computed)。
而后是使用centerPages渲染页码按钮组:
<li [ngClass]="{ number: true, active: page == current }" *ngFor="let page of centerPages" (click)="setPage($event, page)" >{{ page }}</li>
至此三大框架的Pager组件都已实现,于是Pagination组件也告一段落。
最后作一个总结,大体对比下Vue/React/Angular三大框架开发组件的差异。
框架 |
从外向内通信 |
从内向外通信 |
编程范式 |
列表渲染 |
条件渲染 |
事件绑定 |
内部状态 |
插槽定义方式 |
计算属性 |
监听外部传入的参数变量 |
Vue |
props |
$emit() |
响应式 |
v-for指令 |
v-if指令 |
v-bind:event(简写@event) |
data |
<slot> |
computed |
watch |
React |
props |
props |
函数组件 |
{}包裹map |
{}包裹三目运算符 |
onEvent |
useState |
props.children |
直接写 |
useEffect |
Angular |
@Input() |
@Output() emit() |
面向对象 |
*ngFor指令 |
*ngIf指令 |
(event) |
直接写 |
<ng-content> |
@Input() get |
ngOnChanges |
以上3大框架的Pagination组件源码地址:
https://github.com/kagol/components
本文参考DevUI分页组件写成,该组件源码地址:
https://github.com/DevCloudFE/ng-devui/tree/master/devui/pagination
欢迎你们关注DevUI组件库,给咱们提意见和建议,也欢迎Star。
咱们是DevUI团队,欢迎来这里和咱们一块儿打造优雅高效的人机设计/研发体系。招聘邮箱:muyang2@huawei.com。
文/DevUI Kagol