手把手教你使用Vue/React/Angular三大框架开发Pagination分页组件
<p><img src="https://img2020.cnblogs.com/blog/296720/202012/296720-20201201224807829-197300972.png"></p><p> </p>
<p> </p>
<blockquote>
<p>DevUI是一支兼具设计视角和工程视角的团队,服务于华为云DevCloud平台和华为内部数个中后台系统,服务于设计师和前端工程师。<br>官方网站:devui.design<br>Ng组件库:ng-devui(欢迎Star)</p>
</blockquote>
<h1>引言</h1>
<blockquote>
<p>“他在正午、黄昏,在一天里的许多时刻去感受它、记录它,结果也就让我们看到了那么多的不同。他描绘它的角度没变,但它的面目却极大地改变了。”</p>
</blockquote>
<p>19世纪著名的印象派画家莫奈,喜欢对着同一处景物,分别画出对象在不同时间,不同光线下的色彩变化。</p>
<p>比如不同季节的三株白杨:</p>
<p><img src="https://user-gold-cdn.xitu.io/2020/5/9/171f795a65b9316b?w=1500&h=891&f=jpeg&s=198943"></p>
<p>比如一天中不同时刻的浮翁大教堂:</p>
<p><img src="https://user-gold-cdn.xitu.io/2020/5/9/171f795a65c45b0c?w=1500&h=734&f=jpeg&s=175280"></p>
<p>如果同一个组件,用不同的框架实现,会有什么不同呢?</p>
<p>带着这个想法,我分别选用目前最火的Vue/React/Angular三大框架,去实现一个简单的Pagination分页组件。</p>
<p> </p>
<h1>1 组件需求</h1>
<p>我们要实现的分页组件大致效果如下:</p>
<p><img src="https://user-gold-cdn.xitu.io/2020/5/9/171f795a65d030a5?w=904&h=116&f=png&s=4070"></p>
<p>主要包含以下功能:</p>
<ol>
<li>点击左右分页按钮可以跳转到上一页/下一页;</li>
<li>点击中间的页码按钮可能跳转到相应的页码;</li>
<li>首页尾页需要始终显示出来(如果只有1页则不显示尾页);</li>
<li>除首尾页之外,当前页码左右最多只显示2页(共5页);</li>
<li>页码太多时显示更多页码按钮,点击更多页码按钮跳转5页。</li>
</ol>
<h1>2 模块设计</h1>
<p>从设计稿可以看出,Pagination组件主要由2个模块组成:</p>
<ol>
<li>Button - 左右分页按钮</li>
<li>Pager - 中间的分页器</li>
</ol>
<p><img src="https://user-gold-cdn.xitu.io/2020/5/9/171f795a6f13ce30?w=916&h=216&f=png&s=13064"></p>
<h1>3 空的Pagination组件</h1>
<p>我们采用自上而下的方式创建组件,先创建一个空的Pagination组件。</p>
<blockquote>
<p>注意⚠️</p>
<p>我使用的框架版本号如下:</p>
<p>node@10.15.1</p>
<p>vue-cli@3.7.0</p>
<p>vue@2.6.10</p>
<p>create-react-app@3.0.1</p>
<p>react@16.8.6</p>
<p>angular-cli@7.3.9</p>
<p>angular@7.2.0</p>
</blockquote>
<h2>3.1 Vue版本</h2>
<p>使用Vue CLI创建一个基础Vue项目,并输入npm run serve命令启动起来。</p>
<p>然后在components文件夹新建一个pagination文件夹,里面新建我们需要的3个组件文件:</p>
<ol>
<li>按钮组件 - Button.vue</li>
<li>分页器组件 - Pager.vue</li>
<li>分页组件 - Pagination.vue</li>
</ol>
<p><img src="https://user-gold-cdn.xitu.io/2020/5/9/171f795a660ed8d4?w=346&h=786&f=png&s=39894"></p>
<p>在Pagination.vue文件中增加以下代码:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight"><template>
<div class="x-pagination">
Pagination组件
</div>
</template>
<script>
export default {
name: 'Pagination',
};
</script></pre>
</div>
</div>
</div>
<p>Vue组件的特点是将HTML/CSS/JavaScript都统一放在一个.vue后缀的文件中。</p>
<p>对于习惯将HTML/CSS/JavaScript分开编写的前端开发者来说,显得非常自然,加上Vue的语法非常简洁,入门门槛比较低,所以2014年一经推出,很快便席卷全球。</p>
<p>在views/Home.vue中使用Pagination组件:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight"><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></pre>
</div>
</div>
</div>
<p>组件的使用方式也和普通HTML元素很类似:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight"><Pagination /></pre>
</div>
</div>
</div>
<p>需要注意的是使用Vue局部组件之前需要在components中声明该组件。</p>
<p>这只是一个空组件,只显示了“Pagination组件”文字,没有太大的意义,不过不要着急,后面我们会一步步完善该组件,实现我们想要的功能,并能不断扩展和演进。在继续开发Vue版本的Pagination组件之前,我们先来看看其他框架如何实现和使用一个组件。</p>
<p>以下是显示效果:</p>
<p><img src="https://user-gold-cdn.xitu.io/2020/5/9/171f795a661c45d5?w=932&h=854&f=png&s=50036"></p>
<h2>3.2 React版本</h2>
<p>先来看看React框架,我们同样使用Create React App创建一个基础的React项目,并输入命令npm start命令启动。</p>
<p>和Vue项目一样,创建以下3个组件文件:</p>
<ol>
<li>按钮组件 - Button.js</li>
<li>分页器组件 - Pager.js</li>
<li>分页组件 - Pagination.js</li>
</ol>
<p><img src="https://user-gold-cdn.xitu.io/2020/5/9/171f795a95132c47?w=350&h=754&f=png&s=37759"></p>
<p>在Pagination.js文件中增加以下代码:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">import React from 'react';
function Pagination() {
return (
<div className="x-pagination">
Pagination组件
</div>
);
}
export default Pagination;</pre>
</div>
</div>
</div>
<p>可以看到React开发组件的方式和Vue相差非常大,React推崇函数式编程(FP,Functional Programming),每个React组件都是一个函数,HTML/CSS/JavaScript都在函数里面,在函数里面返回模板内容。</p>
<p>需要注意⚠️的是在React中HTML元素的class需要写成className,原因是class是JavaScript中的保留关键字,而React使用的JSX是JavaScript的扩展,使用class会导致命名冲突。</p>
<p>React这种写法很特别,初学者可能会不太习惯,不过一旦用习惯了,会觉得非常爽,觉得一切都非常合理,组件就应该这样写。</p>
<p>在App.js中使用Pagination组件:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">import React from 'react';
import Pagination from './components/pagination/Pagination';
import './App.scss';
function App() {
return (
<div className="App">
<Pagination />
</div>
);
}
export default App;</pre>
</div>
</div>
</div>
<p>使用React组件的方式也很简单,和使用普通HTML元素类似:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight"><Pagination /></pre>
</div>
</div>
</div>
<p>显示的效果与Vue版本无异。</p>
<h2>3.3 Angular版本</h2>
<p>和Vue/React这种专注View视图层的轻量级框架不同,Angular是一个很重的框架,配备非常完整,Web开发过程中你需要的一切,Angular框架都给你提供好了,你只需要随手取用即可。</p>
<p>我们一起来看看怎么开发一个Angular组件吧。</p>
<p>同样是使用Angular CLI创建一个基础的Angular项目,并输入命令npm start命令启动。</p>
<p>和React/Vue组件不同,Angular组件不能单独使用,需要包一层Module,因此我们需要创建1个模块文件和3个组件文件:</p>
<ol>
<li>Pagination模块 - pagination.module.ts</li>
<li>按钮组件 - button.component.ts</li>
<li>分页器组件 - pager.component.ts</li>
<li>分页组件 - pagination.component.ts</li>
</ol>
<p>HTML/CSS可以放在ts文件里面,也可以放在单独的文件里。</p>
<p>一般而言,HTML/CSS内容较少时,会将它们放到ts文件里。</p>
<p><img src="https://user-gold-cdn.xitu.io/2020/5/9/171f795aa3d52025?w=478&h=886&f=png&s=67091"></p>
<p>先创建Pagination模块,在pagination.module.ts文件中增加以下代码:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">import { NgModule } from "@angular/core";
@NgModule()
export class PaginationModule { }</pre>
</div>
</div>
</div>
<p>然后是创建Pagination组件,在pagination.component.ts文件中增加以下代码:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">import { Component } from "@angular/core";
@Component({
selector: 'x-pagination',
template: `
<div class="x-pagination">
Pagination组件
</div>
`,
})
export class PaginationComponent { }</pre>
</div>
</div>
</div>
<p>Angular和Vue/React非常明显的区别已经显示出来:</p>
<p>首先是组件需要依托于Module存在;</p>
<p>然后是不管是定义Module还是Component都需要使用装饰器;</p>
<p>比如定义一个Angular模块需要使用@NgModule装饰器,定义一个Angular组件需要使用@Component装饰器。</p>
<p>还有就是Angular推崇的是面向对象的编程范式,Angular里面的几乎一切都是类和对象,除了刚才一经介绍的模块和组件,还有服务(Service)、管道(Pipe)等,都是类(class)。</p>
<p>为了使用Pagination组件,我们需要先导入Pagination模块,并声明Pagination组件,在app.module.ts文件中增加以下代码:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">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:
})
export class AppModule</pre>
</div>
</div>
</div>
<p>然后就能使用Pagination组件了,在app.component.ts文件中增加以下代码:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight"><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></pre>
</div>
</div>
</div>
<p>使用Angular组件的方式和普通的HTML元素类似:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight"><x-pagination></x-pagination></pre>
</div>
</div>
</div>
<p>显示的效果与Vue/React一样。</p>
<h1>4 List组件和假数据</h1>
<p>在添加实际的分页功能之前我们需要先做一个List组件,用来模拟分页数据的展示。</p>
<p>根据我们之前介绍的3个框架实现组件的方式,然后稍微增加些额外的知识,我们就能很快做一个数据渲染组件List。</p>
<p>还是先看Vue框架吧。</p>
<h2>4.1 Vue版本</h2>
<p>新建List.vue组件文件,输入以下代码:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight"><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></pre>
</div>
</div>
</div>
<p>在template模板部分,我们使用Vue的v-for指令,在li元素中循环lists数组,并将name值显示出来。其中的:key是v-bind:key的简写形式,为元素绑定唯一的key值,用于DOM对比时的性能优化。</p>
<p>1) 通过props传入数据</p>
<p>原本我打算直接将lists的值放到props中,通过外部传进来,如下:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight"><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></pre>
</div>
</div>
</div>
<p>这样有一个问题,就是外部传入的lists如果发生变化,template里绑定的lists不会相应的变化。</p>
<p>2) 维护内部状态</p>
<p>为了监听props中的值的变化,我把lists放到组件内部状态中(data),外部传入的数据叫dataSource,如下:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight"><script>
export default {
name: 'List',
props: {
dataSource: Array
},
data() {
return {
lists: this.dataSource
}
},
};
</script></pre>
</div>
</div>
</div>
<p>3) 监听外部props的变化</p>
<p>然后监听dataSource的变化,当dataSource变化时,将新值赋值给lists:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">watch: {
// 对dataSource进行监听,如果发生变化则重新将新值赋给lists
dataSource: {
handler(newValue, oldValue) {
this.lists = newValue;
}
}
}</pre>
</div>
</div>
</div>
<p>传入List组件的lists数组如下:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">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' },
];</pre>
</div>
</div>
</div>
<p>使用List组件展示数据:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight"><List :data-source="lists" /></pre>
</div>
</div>
</div>
<p>这里需要注意⚠️的是,所有绑定的数据需要使用短横线命名法,比如上面的data-source,对应data中驼峰命名法的dataSource。</p>
<p>展示的效果如下:</p>
<p><img src="https://user-gold-cdn.xitu.io/2020/5/9/171f795aa831ab3e?w=292&h=504&f=png&s=7953"></p>
<h2>4.2 React版本</h2>
<p>React编写的是函数组件,props的变化会直接反映到模板中,不需要单独监听,所以写起来非常简洁:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">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</pre>
</div>
</div>
</div>
<p>外部数据通过函数的props参数传入,这里将props进行了对象解构,直接取到了dataSource字段。</p>
<p>还有一点和Vue不太一样,就是React是函数式编程的写法,列表数据的渲染不需要v-for之类的指令,而是通过数组的map方法,直接返回相应的li元素即可,看着非常自然。其中li元素上绑定的key值与Vue中key值的作用类似。</p>
<p>使用方式和Vue的类似:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight"><List dataSource={dataSource} /></pre>
</div>
</div>
</div>
<h2>4.3 Angular版本</h2>
<p>Angular稍微麻烦些,需要同时定义Module和Component:</p>
<ol>
<li>List模块 - list.module.ts</li>
<li>List组件:list.component.ts</li>
</ol>
<p>先编写list.module.ts:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">import { NgModule } from "@angular/core";
@NgModule()
export class ListModule { }</pre>
</div>
</div>
</div>
<p>然后编写List组件list.component.ts:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">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;
}
}</pre>
</div>
</div>
</div>
<p>Angular和Vue/React的差别比较大:</p>
<ol>
<li>一是外部传参方式不同,Angular使用@Input这个装饰器表示外部参数;</li>
<li>二是Angular使用ngFor指令渲染列表数据;</li>
<li>三是Angular优化DOM对比的方式是使用trackBy。</li>
</ol>
<p>Angular组件的使用方式,倒是和其他框架大同小异:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight"><x-list ="dataSource"></x-list></pre>
</div>
</div>
</div>
<h1>5 基本分页功能</h1>
<p>接下来我们开始给Pagination组件添加实际的分页功能。</p>
<p>添加分页功能之前,我们先设计好Pagination组件的API:</p>
<ol>
<li>数据总数 - total</li>
<li>每页数据数 - defaultPageSize</li>
<li>当前页码 - defaultCurrent</li>
<li>页码改变事件 - onChange</li>
</ol>
<p>total和defaultPageSize两个参数可以合并为一个参数totalPage(总页码),不过考虑到后续的可扩展性(比如需要改变pageSize),将其拆分开来。</p>
<p>实现分页按钮分以下步骤:</p>
<ol>
<li>实现一个通用的按钮组件</li>
<li>在分页组件中使用按钮组件</li>
<li>使用Pagination组件对List进行分页</li>
</ol>
<h2>5.1 Vue版本</h2>
<h3>5.1.1 实现通用的按钮组件</h3>
<p>通过前面编写的空的Pagination组件和List组件,相信大家对Vue组件都很熟悉了。</p>
<p>新建一个Button.vue组件文件,编写以下代码:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight"><template>
<button type="button" @click="$emit('click')"><slot></slot></button>
</template>
<script>
export default {
name: 'Button',
};
</script></pre>
</div>
</div>
</div>
<p>这里要特别注意的是:</p>
<ol>
<li>Vue组件向外暴露事件的方式:使用$emit方法;</li>
<li>还有就是Vue定义插槽的方式是使用<slot>标签。</li>
</ol>
<p>其实以上的写法是一种简写形式,实际应该是这样:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight"><template>
<button type="button" @click="click()"><slot></slot></button>
</template>
<script>
export default {
name: 'Button',
methods: {
click() {
this.$emit('click');
}
},
};
</script></pre>
</div>
</div>
</div>
<p>$emit是Vue组件实例的是一个方法,用于组件对外暴露事件和传递数据,后面会看到传参的例子。</p>
<h3>5.1.2 在Pagination组件中使用Button组件</h3>
<p>做了这么多准备工作,终于可以做些实际的功能。</p>
<p>还记得之前我们编写了一个空的Pagination组件吗?这时我们可以往里面写点功能了。</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight"><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></pre>
</div>
</div>
</div>
<p>将之前的文字“Pagination组件”删掉,加上上一页(<)/下一页(>)两个翻页按钮,另外我们也将当前页码current展示在两个翻页按钮中间,这样我们能更清楚当前处于第几页。</p>
<p>由于左尖括号与HTML标签的左尖括号冲突,不能直接使用,需要使用HTML实体字符&lt;代替。</p>
<p>之前设计的Pagination组件的API参数都放到props里面:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">// 接口定义 props
props: {
defaultCurrent: Number, // 默认当前页码
defaultPageSize: Number, // 默认每页数据数
total: Number, // 数据总数
}</pre>
</div>
</div>
</div>
<p>我们定义了一个组件内部属性current,用于存放动态的页码:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">// 组件内部状态 data
data() {
return {
current: this.defaultCurrent,
}
}</pre>
</div>
</div>
</div>
<p>需要注意⚠️的是,data属性使用的是函数形式,在函数内部返回一个对象,current定义在该对象里面,这样可以确保每个实例可以维护一份被返回对象的独立的拷贝,具体原因可以参考官网的解释。</p>
<p>另外我们还定义了一个计算属性,用于获取总页码totalPage(限制页码边界时需要用到):</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">// 计算属性
computed: {
totalPage: function () {
return Math.ceil(this.total / this.defaultPageSize);
},
}</pre>
</div>
</div>
</div>
<p>最后定义了一个内部方法setPage,用于改变页码:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">// 内部方法定义
methods: {
setPage(page) {
if (page < 1) return; // 限制上一页翻页按钮的边界
if (page > this.totalPage) return; // 限制下一页翻页按钮的边界
this.current = page;
this.$emit('change', this.current);
},
}</pre>
</div>
</div>
</div>
<p>当点击上一页/下一页翻页按钮时都会调用该方法,传入改变后的页码值。</p>
<p>如果是上一页,则传入current - 1:</p>
<p><Button class="btn-prev" @click="setPage(current - 1)">&lt;</Button></p>
<p>下一页则是current + 1:</p>
<p><Button class="btn-next" @click="setPage(current + 1)">></Button></p>
<p>setPage中除了设置当前页码之外,还将页码改变事件发射出去,并将当前页码传到组件外部。</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">this.$emit('change', this.current);</pre>
</div>
</div>
</div>
<p>另外也增加了一些限制翻页边界的逻辑,避免翻页时超过页码的边界,导致不必要的Bug:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">if (page < 1) return; // 限制上一页翻页按钮的边界
if (page > this.totalPage) return; // 限制下一页翻页按钮的边界</pre>
</div>
</div>
</div>
<h3>5.1.3 使用Pagination组件对List进行分页</h3>
<p>有了Pagination组件和List组件,就可以使用Pagination对List进行分页展示。</p>
<p>在Home.vue组件中使用Pagination组件。</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight"><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);
}
}
};
</script></pre>
</div>
</div>
</div>
<p>除了defaultCurrent/defaultPageSize/total这3个Pagination组件的参数外,我们在data内部状态中还定义了一个dataList字段,用于动态传入给List组件,达到分页的效果。</p>
<p>在setList方法中将对lists进行分块,并根据当前的页码获取分页数据,并赋值给dataList字段,这样List组件中就会展示相应的分页数据。</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">setList: function(current, pageSize) {
this.dataList = chunk(lists, pageSize);
}</pre>
</div>
</div>
</div>
<p>setList方法在两处进行调用:created生命周期方法和onChange页码改变事件。</p>
<p>created生命周期事件在Vue实例初始化之后,挂载到DOM之前执行,在created事件中我们将第1页的数据赋值给dataList:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">created() {
this.setList(this.defaultCurrent, this.defaultPageSize);
}</pre>
</div>
</div>
</div>
<p>因此List组件将展示第1页的数据:</p>
<p><img src="https://user-gold-cdn.xitu.io/2020/5/9/171f795aab2d563f?w=214&h=256&f=png&s=6126"></p>
<p>onChange事件是Pagination组件的页码改变事件,当点击上一个/下一页翻页按钮时执行,在该事件中可获取到当前的页码current。</p>
<p>我们在该事件中将当前页码的数据赋值给dataList,这样List组件将展示当前页码的数据,从而达到分页效果。</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">onChange(current) {
this.setList(current, this.defaultPageSize);
}</pre>
</div>
</div>
</div>
<p>setList方法调用了chunk方法(作用与Lodash中的chunk方法类似),该方法用于将一个数组分割成指定大小的多个小数组,它的源码如下:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">// 将数组按指定大小分块
export function chunk(arr = [], size = 1) {
if (arr.length === 0) return [];
return arr.reduce((total, currentValue) => {
if (total.length === size) {
total.push();
} else {
total.push(currentValue);
}
return total;
}, [[]]);
}</pre>
</div>
</div>
</div>
<p>比如之前的lists数组,如果按每页3条数据进行分块chunk(lists, 3),则得到的结果如下:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">[
[
{ "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" }
]
]</pre>
</div>
</div>
</div>
<p>最终实现的分页效果如下:</p>
<p><img src="https://user-gold-cdn.xitu.io/2020/5/9/171f795ab0a5167c?w=214&h=256&f=png&s=6126"> <img src="https://user-gold-cdn.xitu.io/2020/5/9/171f795ac08a0f84?w=202&h=276&f=png&s=5492"> <img src="https://user-gold-cdn.xitu.io/2020/5/9/171f795acbb46643?w=198&h=178&f=png&s=3672"></p>
<p>现在做一个小小的总结,为了实现分页功能,我们:</p>
<ol>
<li>先实现了一个通用的按钮组件;</li>
<li>然后使用这个通用组件,在Pagination组件中增加上一页/下一页两个翻页按钮,点击可以改变当前页码current;</li>
<li>接着使用做好的Pagination组件对List列表组件进行分页。</li>
</ol>
<p>接下来我们看下React如何实现以上功能。</p>
<h2>5.2 React版本</h2>
<h3>5.1.1 实现通用的按钮组件</h3>
<p>同样也是先定义一个通用按钮组件Button.js:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">import React from 'react';
function Button({ onClick, children }) {
return (
<button type="button" onClick={ onClick }>{ children }</button>
);
}
export default Button</pre>
</div>
</div>
</div>
<p>通过前面开发的Pagination/List组件,相信大家对React的函数组件并不陌生了。</p>
<p>和Vue不同的是,React不需要对外发射事件之类的操作,传什么事件进来直接就发射出去了;</p>
<p>另一个不同是定义插槽的方式,React使用props.children代表组件标签中间传入的内容。</p>
<h3>5.1.2 在Pagination组件中使用Button组件</h3>
<p>然后使用通用按钮组件,在Pagination组件中增加上一页/下一页两个翻页按钮:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">import React, { useState } from 'react';
import Button from './Button';
function Pagination(props) {
const { total, defaultCurrent, defaultPageSize, onChange } = props;
// 声明一个叫 “current” 的 state 变量,用来保存当前的页码;
// setPage方法是用来改变current的。
const = 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;</pre>
</div>
</div>
</div>
<p>这里引出React 16.8之后一个很重要的概念:React Hooks。</p>
<p>为了在函数组件中定义组件内部状态,从react库中引入了useState这个方法:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">import React, { useState } from 'react';</pre>
</div>
</div>
</div>
<p>useState就是一个Hook,通过在函数组件里调用它来给组件添加一些内部state,React会在重复渲染时保留这个state。</p>
<p>useState会返回一对值:当前状态和一个让你更新它的函数。</p>
<p>useState唯一的参数就是初始state,这里是默认当前页码(defaultCurrent),这个初始 state 参数只有在第一次渲染时会被用到。</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">const = useState(defaultCurrent);</pre>
</div>
</div>
</div>
<p>当点击上一页/下一页翻页按钮时,我们调用了setPage方法,传入新的页码,从而改变current当前页码,实现分页功能。</p>
<p>另外也和Vue版本一样,通过调用onChange方法将页码改变事件发射出去,并将当前页码传递到组件之外。</p>
<p>如果是上一页:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight"><Button className="btn-prev" onClick={() => {
if (current < 2) return;
setPage(current - 1);
onChange(current - 1);
}}>&lt;</Button></pre>
</div>
</div>
</div>
<p>如果是下一页:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight"><Button className="btn-next" onClick={() => {
if (current >= totalPage) return;
setPage(current + 1);
onChange(current + 1);
}}>></Button></pre>
</div>
</div>
</div>
<h3>5.1.3 使用Pagination组件对List进行分页</h3>
<p>Pagination组件做好了,我们就可以使用它来给List列表组件进行分页啦。</p>
<p>在App.js中引入List和Pagination组件:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">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)
const = useState(chunk(lists, defaultPageSize));
return (
<div className="App">
<List dataSource={dataSource} />
<Pagination total={lists.length} defaultCurrent={defaultCurrent} defaultPageSize={defaultPageSize} onChange={current => {
// 页码改变时,重新设置当前的分页数据
setLists(chunk(lists, defaultPageSize));
}} />
</div>
);
}
export default App;</pre>
</div>
</div>
</div>
<p>同样也是定义了一个List组件的数据源(使用useState这个React Hook):dataSource,默认设置为第一页的数据:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">// 设置List默认分页数据:第一页的数据chunk(lists, defaultPageSize)
const = useState(chunk(lists, defaultPageSize));</pre>
</div>
</div>
</div>
<p>当页码改变时,Pagination的onChange事件能捕获到并执行,该事件中可以拿到当前页码current,这时我们可以通过调用useState的第2个返回值——setLists方法——来改变dataSource数据源,实现分页功能:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight"><Pagination ... onChange={current => {
// 页码改变时,重新设置当前的分页数据
setLists(chunk(lists, defaultPageSize));
}} /></pre>
</div>
</div>
</div>
<p>在组件内维护状态的方式,React和Vue相差较大,这里做一个简单的对比:</p>
<p> </p>
<div>
<table>
<tbody>
<tr>
<td> </td>
<td>
<p>组件内部状态存放位置</p>
</td>
<td>
<p>改变组件内部状态的方式</p>
</td>
</tr>
<tr>
<td>
<p>React</p>
</td>
<td>
<p>useState第1个返回值。</p>
<p>const [<strong class="juejin-editor-bold">state</strong>, setState] = useState(initialState];</p>
</td>
<td>
<p>useState第2个返回值(一个方法)。</p>
<p>const = useState(initialState];</p>
</td>
</tr>
<tr>
<td>
<p>Vue</p>
</td>
<td>
<p>data方法中。</p>
<p>data() {</p>
<p> return {</p>
<p> state: [],</p>
<p> }</p>
<p>}</p>
</td>
<td>
<p>methods对象中。</p>
<p>methods: {</p>
<p> setState: function() {</p>
<p> // 执行具体的代码</p>
<p> }</p>
<p>}</p>
</td>
</tr>
</tbody>
</table>
</div>
<p>另外还有一个需要注意⚠️:</p>
<p>在Vue中,为了初始化List的数据源,没法直接在data中写,比如:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">data() {
return {
dataList: chunk(lists, this.defaultPageSize),
}
}</pre>
</div>
</div>
</div>
<p>而是必须在created初始化方法中写:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">created() {
this.dataList = chunk(lists, this.defaultPageSize);
}</pre>
</div>
</div>
</div>
<p>而在React中则显得简洁和自然许多:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">// 设置List默认分页数据:第一页的数据
const = useState(chunk(lists, defaultPageSize);</pre>
</div>
</div>
</div>
<p>不过React这种写法对初学者是不友好的,习惯之后会觉得很舒服。</p>
<h2>5.3 Angular版本</h2>
<h3>5.1.1 实现通用的按钮组件</h3>
<p>最后来看下Angular如何实现分页功能,思路都一样,先定义一个通用按钮组件button.component.ts:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">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();
}
}</pre>
</div>
</div>
</div>
<p>Angular和React/Vue的差别是很明显的:</p>
<ol>
<li>一是绑定事件的语法不同;</li>
<li>二是定义插槽的方式不同;</li>
<li>三是暴露外部事件和发射外部事件的方式不同。</li>
</ol>
<p>这里也简单做一个对比:</p>
<p> </p>
<div>
<table>
<tbody>
<tr>
<td> </td>
<td>
<p>绑定事件</p>
</td>
<td>
<p>定义插槽</p>
</td>
<td>
<p>外部事件</p>
</td>
</tr>
<tr>
<td>
<p>Vue</p>
</td>
<td>
<p>v-on指令(简写形式:@)</p>
</td>
<td>
<p><slot>标签</p>
</td>
<td>
<p>$emit()</p>
</td>
</tr>
<tr>
<td>
<p>React</p>
</td>
<td>
<p>props传递</p>
<p>props.onClick</p>
</td>
<td>
<p>props.children</p>
</td>
<td>
<p>props传递,无需发射</p>
</td>
</tr>
<tr>
<td>
<p>Angular</p>
</td>
<td>
<p>括号符()</p>
<p>(click)="btnClick()"</p>
</td>
<td>
<p><ng-content>标签</p>
</td>
<td>
<p>@Output()+emit()</p>
</td>
</tr>
</tbody>
</table>
</div>
<h3>5.1.2 在Pagination组件中使用Button组件</h3>
<p>现在模板中使用通用按钮组件pagination.component.html:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight"><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></pre>
</div>
</div>
</div>
<p>然后在pagination.component.ts中定义具体逻辑:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">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);
}
}</pre>
</div>
</div>
</div>
<p>和Vue/React一样,定义组件接口/计算属性/内部状态/组件方法,只是具体的语法不同,语法上的对比前面已经说明,不再赘言。</p>
<p>下面直接介绍如何使用Pagination组件对List进行分页。</p>
<h3>5.1.3 使用Pagination组件对List进行分页</h3>
<p>在app.component.html中引入Pagination/List两个组件:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight"><x-list ="dataSource"></x-list>
<x-pagination
="total"
="defaultCurrent"
="pageSize"
(onChange)="onChange($event)"
></x-pagination></pre>
</div>
</div>
</div>
<p>在app.component.ts中定义具体逻辑:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">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);
}
}</pre>
</div>
</div>
</div>
<p>思路也是一样的,定义一个List组件的数据源dataSource,组件初始化(ngOnInit)时给dataSource设置初始分页数据(第一页数据),然后在页码改变时重新设置dataSource的值,不再赘言。</p>
<p>只是有一些差异需要注意⚠️:</p>
<ol>
<li>Angular的初始化方法是ngOnInit,Vue是created;</li>
<li>Angular绑定属性的方式是使用中括号[],Vue是使用v-bind指令(或者简写方式:key)。</li>
</ol>
<p>至此三大框架实现基本分页功能的方法及其差异都已介绍完毕,后一节将介绍本文最核心的内容:分页器的实现。</p>
<h1>6 分页器组件Pager</h1>
<p>我们再来回顾下分页组件的模块图:</p>
<p><img src="https://user-gold-cdn.xitu.io/2020/5/9/171f795ae2e9016b?w=916&h=216&f=png&s=13064"></p>
<p>中间显示页码的部分就是分页器,它的核心是页码显示和页码省略的逻辑。</p>
<h2>6.1 页码显示策略</h2>
<p>为了方便地跳转到任意页码,却又不至于在页面中显示太多页码,页码并不是始终全部显示出来的,而是在页码少时全部显示,页码多时只显示部分页码。这就存在显示策略问题。</p>
<p>我们从当前页码出发,比如模块图中当前页码是第5页:</p>
<p><img src="https://user-gold-cdn.xitu.io/2020/5/9/171f795ae4664dd1?w=732&h=90&f=png&s=3274"></p>
<p>那么以该页码为中心,两边显示一定的页码,比如两边各显示2页;</p>
<p>另外首页和尾页需要始终显示出来,方便回到首页和跳转到尾页;</p>
<p>首页到第3页中间的页码以及第7页到尾尾的页码都隐藏起来,并且支持点击左/右更多按钮,快捷跳转多页(比如5页)的功能。</p>
<p>另外需要考虑页码少的情况,如果只有8页怎么显示呢?</p>
<p>很简单,直接去掉右边的更多按钮就好:</p>
<p><img src="https://user-gold-cdn.xitu.io/2020/5/9/171f795ae7642bac?w=734&h=88&f=png&s=3524"></p>
<p>如果当前页码在第4页呢?去掉左边的更多按钮,显示右边的更多按钮即可:</p>
<p><img src="https://user-gold-cdn.xitu.io/2020/5/9/171f795aeb8c0936?w=736&h=94&f=png&s=3606"></p>
<p>以上就是全部的页码显示策略。</p>
<p>现简述如下:</p>
<ol>
<li>首页尾页需要始终显示出来(如果只有1页则不显示尾页);</li>
<li>除首尾页之外,当前页码左右最多只显示2页(共5页);</li>
<li>其他页码折叠起来,用更多按钮代替。</li>
</ol>
<p>接下来看看如何用三大框架实现这个逻辑。</p>
<h2>6.2 Vue版本</h2>
<h3>6.2.1 组件接口设计</h3>
<p>编写Pager分页器组件之前,还是设计好组件的API:</p>
<ol>
<li>总页数 - totalPage</li>
<li>默认当前页码 - defaultCurrent</li>
<li>页码改变事件 - onChange</li>
</ol>
<h3>6.2.2 基本模板框架</h3>
<p>然后先写好模板,在Pager.vue的<template>中编写以下代码:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight"><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></pre>
</div>
</div>
</div>
<p>再在<script>中写基本的逻辑:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight"><script>
import Vue from 'vue';
export default {
name: 'Pager',
// 组件接口定义
props: {
totalPage: Number, // 总页数
defaultCurrent: Number, // 默认当前页码
},
};
</script></pre>
</div>
</div>
</div>
<p>搭好基本框架之后,我们采取最小可用产品(Minimum Viable Product,MVP)的思路:</p>
<p><img src="https://user-gold-cdn.xitu.io/2020/5/9/171f795afc23a1aa?w=1500&h=1060&f=jpeg&s=66578"></p>
<p>分3步实现分页器功能:</p>
<ol>
<li>第1步 实现首尾翻页</li>
<li>第2步 实现快捷分页</li>
<li>第3步 实现分页按钮组</li>
</ol>
<p><img src="https://user-gold-cdn.xitu.io/2020/5/9/171f795b048e2219?w=1500&h=266&f=png&s=41271"></p>
<h3>6.2.3 第1步:首/尾页翻页逻辑</h3>
<p>先显示第1步:首页尾页的显示和跳页逻辑:</p>
<p>首页</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight"><li
class="number"
:class="{ active: this.current == 1 }"
@click="setPage(1)"
>1</li></pre>
</div>
</div>
</div>
<p>尾页</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight"><li
class="number"
:class="{ active: this.current == totalPage }"
v-if="totalPage !== 1"
@click="setPage(totalPage)"
>{{ totalPage }}</li></pre>
</div>
</div>
</div>
<p>由于当前页码有可能从Pager组件外部改变(上一页/下一页按钮),因为需要监听defaultCurrent的变化,需要增加组件内部状态current代替defaultCurrent:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">data() {
return {
current: this.defaultCurrent, // 当前页码
}
}</pre>
</div>
</div>
</div>
<p>然后监听defaultCurrent,当外部传入的defaultCurrent发生变化时,将新值赋值给current:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">watch: {
defaultCurrent: {
handler(newValue, oldValue) {
this.current = newValue;
}
}
}</pre>
</div>
</div>
</div>
<p>接着定义翻页方法:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">methods: {
setPage(page) {
// 对页码进行限制,不能超出的范围
let newPage = page;
if (page < 1) newPage = 1;
if (page > this.totalPage) newPage = this.totalPage;
this.current = newPage; // 设置当前页码
this.$emit('change', this.current); // 向外发射页码改变事件
}
}</pre>
</div>
</div>
</div>
<p>显示的效果如下:</p>
<p><img src="https://user-gold-cdn.xitu.io/2020/5/9/171f795b18d5fe0d?w=410&h=90&f=png&s=1017"></p>
<h3>6.2.4 在Pagination组件中使用Pager组件</h3>
<p>我们可以在Pagination组件中试试初版的Pager。</p>
<p>在Pagination.vue中,去掉之前页码显示的那一行代码,使用Pager组件替代:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight"><template>
<div class="m-pagination">
<Button class="btn-prev" @click="setPage(current - 1)">&lt;</Button>
// 去掉该行 {{ current }},替换成以下Pager组件
<Pager :total-page="totalPage" :default-current="current" @change="onChange"></Pager>
<Button class="btn-next" @click="setPage(current + 1)">></Button>
</div>
</template></pre>
</div>
</div>
</div>
<p>然后增加Pager的onChange页码改变的回调事件:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">methods: {
onChange(current) {
this.current = current; // 设置当前页码
this.$emit('change', this.current); // 向Pagination组件外发射页码改变事件
}
}</pre>
</div>
</div>
</div>
<p>可以试试首/尾页的翻页效果:</p>
<p><img src="https://user-gold-cdn.xitu.io/2020/5/9/171f795b1b56dbf7?w=574&h=220&f=png&s=4698"></p>
<h3>6.2.5 第2步:增加左/右更多按钮的翻页功能</h3>
<p>有了首尾页的翻页还不够,还需要继续完善更多按钮的快捷翻页功能。</p>
<p>先梳理下更多按钮的显示逻辑:</p>
<ol>
<li>中间按钮一共5页,加上首尾按钮2页,一共7页,也就是说只有大于7页,才有可能显示更多按钮;</li>
<li>左右更多按钮会随着当前页码的不同而显示或隐藏,以第4页和倒数第4页为界;</li>
<li>当页码大于第4页时,应该显示左边更多按钮;</li>
<li>当页码小于倒数第4页,都应该显示右边更多按钮。</li>
</ol>
<p>具体实现如下:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight"><!-- 左更多按钮 -->
<li
class="more left"
v-if="totalPage > 7 && current >= 5"
></li>
<!-- 右更多按钮 -->
<li
class="more right"
v-if="totalPage > 7 && current <= totalPage - 4"
></li></pre>
</div>
</div>
</div>
<p>不过我们不想写死这些数字,假设中间页码数为centerSize(这里是5),可以重构成:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight"><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></pre>
</div>
</div>
</div>
<p>接着是增加快捷翻页事件:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight"><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></pre>
</div>
</div>
</div>
<p>注意⚠️:为了不写死每次快捷跳转的页码,我们用jumpSize保存该值。</p>
<p>接下来我们可以看看快捷翻页的效果,为了清楚看出当前处于哪一页,我们暂时将中间为哦未实现的页码按钮组显示成当前页码:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight"><!-- 中间页码组 -->
<li class="number">{{ current }}</li></pre>
</div>
</div>
</div>
<p>初始在第1页: <img src="https://user-gold-cdn.xitu.io/2020/5/9/171f795b20326a04?w=498&h=258&f=png&s=6065"></p>
<p>点击右更多按钮之后(跳转到第6页): <img src="https://user-gold-cdn.xitu.io/2020/5/9/171f795b303f1e39?w=574&h=290&f=png&s=7175"></p>
<p>再点击右更多按钮(跳转到第11页): <img src="https://user-gold-cdn.xitu.io/2020/5/9/171f795b30033597?w=522&h=248&f=png&s=6061"></p>
<p>点击左更多按钮则又回到第6页,完美达到预期。</p>
<h3>6.2.6 第3步:实现中间的页码按钮组</h3>
<p>中间页码组centerPages是一个长度在之间的数组,它的值由总页码totalPage和当前页码current共同决定,计算规则如下:</p>
<ol>
<li>如果总页码小于等于7,则centerPages是除首尾页之外的所有页码;</li>
<li>如果总页码大于7,则centerPages是以current为中心,左右各加两页组成的页码数组。</li>
</ol>
<p>将centerPages定义为计算属性,具体实现如下:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">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;
}
}</pre>
</div>
</div>
</div>
<p>有了中间页码数组,就可以渲染中间页码组:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight"><!-- 中间页码组 -->
<li
class="number"
v-for="(page, index) in centerPages"
:key="index"
>{{ page }}</li></pre>
</div>
</div>
</div>
<p>接着为其增加active类(用于高亮)和绑定点击事件(用于跳转到相应的页码):</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight"><!-- 中间页码组 -->
<li
class="number"
:class="{ active: current === page }"
v-for="(page, index) in centerPages"
:key="index"
@click="setPage(page)"
>{{ page }}</li></pre>
</div>
</div>
</div>
<p>最终效果如下:</p>
<p>只有1页的情况: <img src="https://user-gold-cdn.xitu.io/2020/5/9/171f795b43456a50?w=244&h=354&f=png&s=7740"></p>
<p><=7页的情况: <img src="https://user-gold-cdn.xitu.io/2020/5/9/171f795b49adba41?w=756&h=358&f=png&s=13359"></p>
<p>>7页且当前页码<=4页的情况: <img src="https://user-gold-cdn.xitu.io/2020/5/9/171f795b4ec45117?w=832&h=296&f=png&s=7884"></p>
<p>>7页且当前页码>4页的情况: <img src="https://user-gold-cdn.xitu.io/2020/5/9/171f795b524abb10?w=908&h=302&f=png&s=8695"></p>
<p>至此,Vue版本分页器组件已全部实现,整个Pagination组件也全部实现。</p>
<p>接下来看看React/Angular如何实现分页器吧。</p>
<h2>6.3 React版本</h2>
<p>同样采MVP的思路,我们按以下步骤开发Pager分页器组件:</p>
<ol>
<li>搭建基本模板框架</li>
<li>实现首尾页翻页</li>
<li>实现更多按钮快捷翻页</li>
<li>实现页码按钮组翻页</li>
</ol>
<h3>6.3.1 基本模板框架</h3>
<p>我们先搭建基本模板框架,在Pager.js中编写以下代码:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">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;</pre>
</div>
</div>
</div>
<p>这只是一个空壳子,什么都做不了,接下来我们加点实际的功能。</p>
<h3>6.3.2 第1步:首/尾页翻页逻辑</h3>
<p>增加首尾页显示条件、高亮条件和翻页功能。</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">import React, { useState } from 'react';
function Pager({ totalPage, defaultCurrent, onChange }) {
// 使用useState定义内部状态:当前页码current
const = 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;</pre>
</div>
</div>
</div>
<p>值得注意的是条件渲染的写法,React和Vue还是有点区别的:</p>
<ol>
<li>React是直接用大括号{}包裹,然后像写JS一样写分支判断</li>
<li>Vue在HTML元素中使用的是v-if指令进行分支判断</li>
</ol>
<p>另外就是Vue中有标签class绑定的功能,而React没有类似的功能,需要通过在{}大括号中写三目运算符来判断高亮。</p>
<p>至此Pager已经可以直接拿去Pagination中使用了,不过只能首页和尾页翻页,接下来继续增强Pager的功能。</p>
<h3>6.3.3 第2步:增加左/右更多按钮的翻页功能</h3>
<p>更多按钮显示的逻辑和Vue版本一样:</p>
<ol>
<li>只有大于7页,才有可能显示更多按钮;</li>
<li>左右更多按钮会随着当前页码的不同而显示或隐藏,以第4页和倒数第4页为界;</li>
<li>当页码大于第4页时,应该显示左边更多按钮;</li>
<li>当页码小于倒数第4页,都应该显示右边更多按钮。</li>
</ol>
<p>左更多按钮:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">const centerSize = 5; // 中间按钮组的页码数
const jumpSize = 5; // 快捷翻页的页数
{
totalPage > centerSize + 2 && current >= centerSize
&& <li className="more left"
onClick={() => {
setPage(current - jumpSize); // 设置快捷翻页后的新页码
onChange(current - jumpSize); // 页码改变时的外部回调事件
}}
></li>
}</pre>
</div>
</div>
</div>
<p>右更多按钮:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">{
totalPage > centerSize + 2 && current <= totalPage - centerSize + 1
&& <li className="more right"
onClick={() => {
setPage(current + jumpSize);
onChange(current + jumpSize);
}}
></li>
}</pre>
</div>
</div>
</div>
<p>最后实现页码按钮组功能。</p>
<h3>6.3.4 第3步:实现中间的页码按钮组</h3>
<p>主要是需要计算好centerPages页码数组,计算逻辑和Vue的一样:</p>
<ol>
<li>如果总页码小于等于7,则centerPages是除首尾页之外的所有页码;</li>
<li>如果总页码大于7,则centerPages是以current为中心,左右各加两页组成的页码数组。</li>
</ol>
<p>先计算centerPages:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">// 计算中间页码数组
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);
}
}</pre>
</div>
</div>
</div>
<p>然后将其显示出来:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">{
centerPages.map((page, index) => {
return (
<li
key={index}
className={'number' + (page == current ? ' active' : '')}
onClick={() => {
setPage(page);
onChange(page);
}}
>{ page }</li>
);
})
}</pre>
</div>
</div>
</div>
<p>列表渲染的方式需要注意⚠️:</p>
<ol>
<li>React依然使用的是大括号包裹,然后用JS的map方法进行迭代;</li>
<li>Vue是在HTML标签中使用v-for指令进行列表渲染。</li>
</ol>
<p>由于Pager中的当前页码有可能通过外部改变(比如上一页/下一页按钮),因为在传入的defaultCurrent变化时,需要动态改变current,这需要借助另一个React Hook——useEffect——来实现,具体代码如下:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">// 外部传入的defaultCurrent变化时,需要重新设置current
useEffect(() => {
setPage(defaultCurrent);
});</pre>
</div>
</div>
</div>
<p>另外需要注意的就是更多按钮快捷翻页可能会越界,需要加以显示,为此我们编写了一个limitPage方法:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">const limitPage = (page) => {
if (page < 1) return 1;
if (page > totalPage) return totalPage;
return page;
}</pre>
</div>
</div>
</div>
<p>在更多按钮的事件中使用:</p>
<p>左更多按钮:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">{
totalPage > centerSize + 2 && current >= centerSize
&& <li className="more left"
onClick={() => {
setPage(limitPage(current - jumpSize)); // 设置快捷翻页后的新页码
onChange(limitPage(current - jumpSize)); // 页码改变时的外部回调事件
}}
></li>
}</pre>
</div>
</div>
</div>
<p>右更多按钮:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">{
totalPage > centerSize + 2 && current <= totalPage - centerSize + 1
&& <li className="more right"
onClick={() => {
setPage(limitPage(current + jumpSize));
onChange(limitPage(current + jumpSize));
}}
></li>
}</pre>
</div>
</div>
</div>
<p>这样就完成了React版本的Pager分页器组件,除了细微语法上的差异外,大部分代码逻辑都是一样的。</p>
<p>接下来即将介绍的Angular版本的Pager也是一样的,大部分逻辑都可以复用。</p>
<h2>6.4 Angular版本</h2>
<p>Angular实现Pager的思路和Vue/React也差不多,就是写法上的差异,同样按MVP的思路,分成以下3个步骤:</p>
<ol>
<li>第1步 实现首尾翻页</li>
<li>第2步 实现快捷分页</li>
<li>第3步 实现分页按钮组</li>
</ol>
<p>先实现首/尾页翻页功能。</p>
<h3>6.4.1 第1步:实现首/尾页翻页逻辑</h3>
<p>先做模板,在pager.component.html中编写以下代码:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight"><ul class="x-pager">
<li ="{
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" ="{
number: true,
active: totalPage == current
}"
(click)="setPage($event, totalPage)"
>{{ totalPage }}</li>
</ul></pre>
</div>
</div>
</div>
<p>然后在pager.component.ts中写具体逻辑:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">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);
}
}</pre>
</div>
</div>
</div>
<h3>6.4.2 第2步:实现左/右更多按钮的翻页功能</h3>
<p>由于用于设置页码的方法setPage前面已经写好了,因此只需要在模板中新加左/右更多按钮即可:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight"><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></pre>
</div>
</div>
</div>
<h3>6.4.3 第3步:实现中间的页码按钮组</h3>
<p>最后是实现页码按钮组,关键还是centerPages数组的计算,计算逻辑可以复用Vue/React的。具体实现如下:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight">@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;
}</pre>
</div>
</div>
</div>
<p>类似Vue中的计算属性(computed)。</p>
<p>然后是使用centerPages渲染页码按钮组:</p>
<div>
<div>
<div>
<pre class="juejin-editor-highlight"><li
="{
number: true,
active: page == current
}"
*ngFor="let page of centerPages"
(click)="setPage($event, page)"
>{{ page }}</li></pre>
</div>
</div>
</div>
<p>至此三大框架的Pager组件都已实现,因而Pagination组件也告一段落。</p>
<p>最后做一个总结,大致对比下Vue/React/Angular三大框架开发组件的差别。</p>
<p> </p>
<div>
<table>
<tbody>
<tr>
<td>
<p>框架</p>
</td>
<td>
<p>从外向内通讯</p>
</td>
<td>
<p>从内向外通讯</p>
</td>
<td>
<p>编程范式</p>
</td>
<td>
<p>列表渲染</p>
</td>
<td>
<p>条件渲染</p>
</td>
<td>
<p>事件绑定</p>
</td>
<td>
<p>内部状态</p>
</td>
<td>
<p>插槽定义方式</p>
</td>
<td>
<p>计算属性</p>
</td>
<td>
<p>监听外部传入的参数变量</p>
</td>
</tr>
<tr>
<td>
<p>Vue</p>
</td>
<td>
<p>props</p>
</td>
<td>
<p>$emit()</p>
</td>
<td>
<p>响应式</p>
</td>
<td>
<p>v-for指令</p>
</td>
<td>
<p>v-if指令</p>
</td>
<td>
<p>v-bind:event(简写@event)</p>
</td>
<td>
<p>data</p>
</td>
<td>
<p><slot></p>
</td>
<td>
<p>computed</p>
</td>
<td>
<p>watch</p>
</td>
</tr>
<tr>
<td>
<p>React</p>
</td>
<td>
<p>props</p>
</td>
<td>
<p>props</p>
</td>
<td>
<p>函数组件</p>
</td>
<td>
<p>{}包裹map</p>
</td>
<td>
<p>{}包裹三目运算符</p>
</td>
<td>
<p>onEvent</p>
</td>
<td>
<p>useState</p>
</td>
<td>
<p>props.children</p>
</td>
<td>
<p>直接写</p>
</td>
<td>
<p>useEffect</p>
</td>
</tr>
<tr>
<td>
<p>Angular</p>
</td>
<td>
<p>@Input()</p>
</td>
<td>
<p>@Output()</p>
<p>emit()</p>
</td>
<td>
<p>面向对象</p>
</td>
<td>
<p>*ngFor指令</p>
</td>
<td>
<p>*ngIf指令</p>
</td>
<td>
<p>(event)</p>
</td>
<td>
<p>直接写</p>
</td>
<td>
<p><ng-content></p>
</td>
<td>
<p>@Input() get</p>
</td>
<td>
<p>ngOnChanges</p>
</td>
</tr>
</tbody>
</table>
</div>
<p> </p>
<p>以上3大框架的Pagination组件源码地址:</p>
<p>https://github.com/kagol/components</p>
<p> </p>
<p>本文参考DevUI分页组件写成,该组件源码地址:</p>
<p>https://github.com/DevCloudFE/ng-devui/tree/master/devui/pagination</p>
<p> </p>
<p>欢迎大家关注DevUI组件库,给我们提意见和建议,也欢迎Star。</p>
<p> </p>
<h1>加入我们</h1>
<p>我们是DevUI团队,欢迎来这里和我们一起打造优雅高效的人机设计/研发体系。招聘邮箱:muyang2@huawei.com。</p>
<p>文/DevUI Kagol</p>
<p> </p>
<div>
<div>
<p>往期文章推荐</p>
<p>《现代富文本编辑器Quill的内容渲染机制》</p>
</div>
<p>《现代富文本编辑器Quill的模块化机制》</p>
<p>《前端开发的积木理论——像搭积木一样做前端开发》</p>
</div>
<p><br><br></p><br><br>
来源:https://www.cnblogs.com/kagol/p/14071347.html
頁:
[1]