1 组件需求和模块设计
我们要实现的分页组件大致效果如下:
组件需求
- 点击左右分页按钮可以跳转到上一页/下一页;
- 点击中间的页码按钮可以跳转到相应的页码;
- 首页尾页需要始终显示出来(如果只有1页则不显示尾页);
- 除首尾页之外,当前页码左右最多只显示2页(共5页);
- 页码太多时显示更多页码按钮,点击更多页码按钮跳转5页。
模块设计
从效果图可以看出,Pagination组件主要由2个模块组成:
- Button - 左右分页按钮
- Pager - 中间的分页器
目录结构
先设计好pagination
组件的目录结构:
├── pagination
| ├── tests // 单元测试
| | └── pagination.test.ts
| ├── index.ts // 入口文件
| └── src // 源码
| ├── components // 子组件
| | ├── icon-arrow-left.tsx // 左箭头图标组件
| | ├── icon-arrow-right.tsx // 右箭头图标组件
| | ├── pager.scss // 分页器组件样式
| | ├── pager.tsx // 分页器组件
| | └── pager-type.ts // 分页器组件类型
| ├── composables // Composables
| | └── use-page.ts
| ├── pagination.scss // 组件样式
| ├── pagination.tsx // 分页组件
| └── pagination-type.ts // 组件类型
2 实现交互逻辑 - usePage
对于分页组件来说,最核心的交互就是分页:
- 当我们点击左右分页按钮时,我们在分页
- 当我们点击中间的页码时,我们也在分页
- 当我们点击快速跳转按钮时,依然在分页
而且这个分页的动作是一种交互,是UI无关的,是这个组件最核心的部分,所以第一步应该是实现这部分逻辑。
实现基础usePage
Vue3中有Composable的概念,我们将分页逻辑做成一个叫usePage
的Composable。
创建一个use-page.ts
文件,写入以下代码:
export default function usePage() {}
一开始usePage
没有任何内容。
分页的逻辑里面有一个状态叫页码,分页的动作就是改变页码:
- 当我们点击上一页,页码减1
- 点击下一页,页码加1
- 点击中间的页码,则跳转到相应的页码
- 点击快速往前跳转按钮,页码减5
- 点击快速往后跳转按钮,页码加5
所以我们应该在usePage
中维护一个页码的变量pageIndex
:
import { ref } from 'vue'
export default function usePage(defaultPageIndex = 1) {
const pageIndex = ref(defaultPageIndex)
return { pageIndex }
}
因为这个页码是动态的,所以我们把它做成响应式的,用ref包裹起来,并且设置默认值defaultPageIndex
,这个默认值是usePage
的唯一参数,最后将pageIndex
导出,这时我们可以在Pagination
组件中使用。
在pagination.ts
中写入以下代码:
import { defineComponent, toRefs, ref } from 'vue'
import usePage from './composables/use-page'
export default defineComponent({
name: 'SPagination',
setup() {
const { pageIndex } = usePage()
return () => (
<div class="s-pagination">
{ pageIndex.value }
</div>
)
}
})
我们只是显示usePage中导出来的页码pageIndex,这是一个静态的页码,传入什么显示什么,比如传入2,则页面显示2。
动态改变页码
我们需要一个方法可以改变页码,比如就叫setPageIndex:
import { ref } from 'vue'
export default function usePage(defaultPageIndex = 1) {
const pageIndex = ref(defaultPageIndex)
const setPageIndex = (current: number) => {
pageIndex.value = current
}
return { pageIndex, setPageIndex }
}
setPageIndex有一个参数,用来设置pageIndex为第几页。
我们在pagination组件中使用下:
import { defineComponent, toRefs, ref } from 'vue'
import usePage from './composables/use-page'
export default defineComponent({
name: 'SPagination',
setup() {
const { pageIndex, setPageIndex } = usePage()
return () => (
<div class="s-pagination">
<button onClick={() => setPageIndex(2)}>{ pageIndex.value }</button>
</div>
)
}
})
我们点击按钮之后,页码就会从第1页变成第2页。
这是跳转到指定页码,如果要跳转到上一页怎么办呢?很简单:
<button onClick={() => setPageIndex(pageIndex.value - 1)}>{ pageIndex.value }</button>
同理快速往前跳转5页呢?
<button onClick={() => setPageIndex(pageIndex.value - 5)}>{ pageIndex.value }</button>
基础usePage的效果
这时最简单的usePage就已经完成了,我们来试试效果吧。
import { defineComponent, toRefs, ref } from 'vue'
import usePage from './composables/use-page'
export default defineComponent({
name: 'SPagination',
setup() {
const { pageIndex, setPageIndex } = usePage()
return () => (
<div class="s-pagination">
<button onClick={() => setPageIndex(pageIndex.value - 1)}>上一页</button>
<button onClick={() => setPageIndex(1)}>1</button>
<button onClick={() => setPageIndex(pageIndex.value - 5)}>...</button>
{ pageIndex.value }
<button onClick={() => setPageIndex(pageIndex.value + 5)}>...</button>
<button onClick={() => setPageIndex(10)}>10</button>
<button onClick={() => setPageIndex(pageIndex.value - 1)}>下一页</button>
</div>
)
}
})
现在我们的分页组件已经具备基本的分页能力:
- 可以跳转到上一页、下一页
- 可以跳转到指定页码
- 可以快速往前、往后跳转N页
做一次小优化吧
我们的usePage现在只有一个setPageIndex方法,所有的分页动作,比如上一页、下一页、快速跳转页码等,都是用的该方法,这会使代码比较难理解,我们来做一个优化吧。
先给usePage增加快速往前或往后跳转N页的功能。
import { ref } from 'vue'
export default function usePage(defaultPageIndex = 1) {
const pageIndex = ref(defaultPageIndex)
const setPageIndex = (current: number) => {
pageIndex.value = current
}
// 新增
const jumpPage = (page: number) => {
pageIndex.value += page
}
return { pageIndex, setPageIndex, jumpPage }
}
这样快速往前、往后5页就可以改成:
<button onClick={() => jumpPage(-5)}>...</button>
{ pageIndex.value }
<button onClick={() => jumpPage(5)}>...</button>
并且我们可以基于jumpPage快速实现上一页、下一页:
import { ref } from 'vue'
export default function usePage(defaultPageIndex = 1) {
const pageIndex = ref(defaultPageIndex)
const setPageIndex = (current: number) => {
pageIndex.value = current
}
const jumpPage = (page: number) => {
pageIndex.value += page
}
// 新增
const prevPage = () => jumpPage(-1)
const nextPage = () => jumpPage(1)
return { pageIndex, setPageIndex, jumpPage, prevPage, nextPage }
}
这样我们的例子就可以简化成:
import { defineComponent, toRefs, ref } from 'vue'
import usePage from './composables/use-page'
export default defineComponent({
name: 'SPagination',
setup() {
const { pageIndex, setPageIndex, jumpPage, prevPage, nextPage } = usePage()
return () => (
<div class="s-pagination">
<button onClick={prevPage}>上一页</button>
<button onClick={() => setPageIndex(1)}>1</button>
<button onClick={() => jumpPage(-5)}>...</button>
{ pageIndex.value }
<button onClick={() => jumpPage(5)}>...</button>
<button onClick={() => setPageIndex(10)}>10</button>
<button onClick={nextPage}>下一页</button>
</div>
)
}
})
3 基础版Pagination
有了usePage
这个内核,我们的基础Pagination
组件就很容易实现啦,先实现一个基础版本吧。
实现基础版分页组件
开发组件的第一步时定义它的输入输出,在pagination-type.ts
文件中定义Pagination
组件的输入。
import { ExtractPropTypes } from 'vue'
export const paginationProps = {
total: {
type: Number,
default: 0,
},
pageSize: {
type: Number,
default: 10,
},
} as const
export type PaginationProps = ExtractPropTypes<typeof paginationProps>
我们定义了两个props:
total
:数据总数,默认为0pageSize
:每页展示的数据条数,默认为10
在pagination.tsx
中添加props:
import { defineComponent, toRefs } from 'vue'
import usePage from './composables/use-page'
import { PaginationProps, paginationProps } from './pagination-type'
export default defineComponent({
name: 'SPagination',
props: paginationProps,
setup(props: PaginationProps) {
// 获取总条数和单页条数
const { total, pageSize } = toRefs(props)
// 相除之后向上取整就是总页数
const totalPage = Math.ceil(total.value / pageSize.value)
const { pageIndex, prevPage, nextPage, setPageIndex } = usePage()
return () => (
<div class="s-pagination">
<button onClick={prevPage}>上一页</button>
<button onClick={() => setPageIndex(1)}>1</button>
{pageIndex.value}
{/* 现在可以设置总页数了 */}
<button onClick={() => setPageIndex(totalPage.value)}>{totalPage.value}</button>
<button onClick={nextPage}>下一页</button>
</div>
)
}
})
这里有一个计算逻辑,就是根据数据总数total
和每页数据条数pageSize
计算总页码数totalPage
。
const totalPage = Math.ceil(total.value / pageSize.value)
这里有一个计算逻辑,就是根据数据总数total和每页数据条数p
使用基础版分页组件
可以使用看下效果:
## 基础功能
设置总条数`total`即可
:::demo 设置total
```vue
<template>
<SPagination :total="50"></SPagination>
</template>
效果如下:
50条数据一共5页,所以尾页显示5,翻页功能也是正常的。
4 实现分页器功能
分页器Pager
指的是分页组件中间那部分,就是下图中蓝框的部分:
需求梳理和结构设计
这块比较复杂,涉及到比较多逻辑判断,我们先梳理下:
- 首页和尾页是常驻的,如果只有1页,则不显示
- 页码按钮有一个最大数量
pagerCount
,上图是7,也就是说最多显示7个页码按钮 - 如果总页数
totalPage
大于pagerCount
,则会出现显示不下的情况,这时显示不下的部分用...
表示,并且这个...
是可以快速往前、往后跳转N页的 - 中间页码应该显示的页码按钮数量在0到
pagerCount-2
之间 - 只有2页的情况下,中间页码按钮数量为0
- 大于等于
pagerCount
的情况下,中间按钮数量等于pagerCount-2
- 当中间页码左边的页数大于2时,应该出现左边的
...
- 当中间页码右边的页数小于
totalPage-3
时,应该出现右边的...
我们先设计好Pager的大致结构:
import { defineComponent, toRefs } from 'vue'
import usePage from './composables/use-page'
import { PaginationProps, paginationProps } from './pagination.type'
export default defineComponent({
name: 'SPagination',
props: paginationProps,
setup(props: PaginationProps) {
const { total, pageSize } = toRefs(props)
const totalPage = Math.ceil(total.value / pageSize.value)
const { pageIndex, prevPage, nextPage, setPageIndex } = usePage()
return () => (
<div class="s-pagination">
<button onClick={prevPage}>上一页</button>
<!-- Pager 部分 start -->
<ul class="s-pager">
<li>1</li>
<li class="more left">...</li>
<li>中间页码</li>
<li class="more right">...</li>
<li>{totalPage.value}</li>
</ul>
<!-- Pager 部分 end -->
<button onClick={nextPage}>下一页</button>
</div>
)
}
})
我们分两步走:
- 先解决首页、尾页、左更多、右更多的渲染条件问题
- 再解决中间页码的渲染问题
先解决首尾问题
<ul class="s-pager">
<!-- 首页是常驻的,不管 -->
<li>1</li>
<!-- 左更多按钮显示的条件有2个: -->
<!-- 1. 总页码totalPage要大于最大页码按钮数量pagerCount -->
<!-- 2. 当前页码大于 Math.ceil(pagerCount / 2) -->
{totalPage.value > pagerCount.value &&
pageIndex.value > Math.ceil(pagerCount.value / 2) && (
<li class="more left">...</li>
)}
<li>中间页码</li>
<!-- 右更多按钮显示的条件也有2个: -->
<!-- 1. 总页码totalPage要大于最大页码按钮数量pagerCount(和左更多按钮相同) -->
<!-- 2. 当前页码小于 totalPage.value - Math.ceil(pagerCount.value / 2) + 1 -->
{totalPage.value > pagerCount.value &&
pageIndex.value <
totalPage.value - Math.ceil(pagerCount.value / 2) + 1 && (
<li class="more right">...</li>
)}
<!-- 尾页显示的条件是:总页码大于1 -->
{ totalPage.value > 1 && <li>{totalPage.value}</li>}
</ul>
我们来测试下这个显示逻辑是否符合预期,主要测试以下用例(默认pageSize为10,pagerCount为7):
用例1:total=1时应该只显示首页,即1
用例2:total=11时,应该显示首页和尾页,即1和2
用例3:total=80 && pageIndex=4,应该显示右更多按钮
用例4:total=80 && pageIndex=5,应该显示左更多按钮
用例5:total=90 && pageIndex=5,应该显示左、右更多按钮
用例6:total=90 && pageIndex=4,应该只显示右更多按钮
用例7:total=90 && pageIndex=6,应该只显示左更多按钮
经过测试,我们前面写的首页、尾页、左更多、右更多的渲染条件是没问题的。
渲染中间页码按钮
接下来解决中间页码按钮的渲染问题。
中间页码由3个变量共同决定:
- 总页码totalPage
- 当前页码pageIndex
- 最大显示页码数pagerCount
所以我们可以编写一个函数,用来获取中间页码数组。
const getCenterPages = (totalPage, pageIndex, pagerCount) => {
const totalPageArr = Array.from(Array(totalPage).keys())
if (totalPage <= pagerCount) {
// totalPageArr: [0,1,2,3,4]
// 总页码较小时,全部显示出来
// [2,3,4]
return totalPageArr.slice(2, totalPage)
} else {
// 总页码较大时,只显示部分页码
const middle = Math.ceil(pagerCount / 2)
// totalPageArr: [0,1,2,3,4,5,6,7,8]
if (pageIndex <= middle) {
// pageIndex=3
// [2,3,4,5,6]
// 左边全显示
return totalPageArr.slice(2, pagerCount)
} else if (pageIndex >= totalPage - middle + 1) {
// pageIndex=6
// [4,5,6,7,8]
// 右边全显示
return totalPageArr.slice(totalPage - pagerCount + 2, totalPage)
} else {
// pageIndex=4
// [2,3,4,5,6]
// 中间显示
return totalPageArr.slice(pageIndex - middle + 2, pageIndex + middle - 1)
}
}
}
有了中间页码数组,我们就可以用它来渲染中间页码啦。
import { defineComponent, toRefs, computed } from 'vue'
import usePage from './composables/use-page'
import { PaginationProps, paginationProps } from './pagination.type'
export default defineComponent({
name: 'SPagination',
props: paginationProps,
setup(props: PaginationProps) {
const pagerCount = ref(7)
const { total, pageSize } = toRefs(props)
const totalPage = Math.ceil(total.value / pageSize.value)
const { pageIndex, prevPage, nextPage, setPageIndex } = usePage()
// +++
const centerPages = computed(() => getCenterPages(totalPage.value, pageIndex.value, pagerCount.value))
return () => (
<div class="s-pagination">
<button onClick={prevPage}>上一页</button>
<!-- Pager 部分 start -->
<ul class="s-pager">
<!-- ... -->
<!-- +++中间页码+++ -->
{ centerPages.value.map(page => <li>{page}</li>) }
<!-- ... -->
</ul>
<!-- Pager 部分 end -->
<button onClick={nextPage}>下一页</button>
</div>
)
}
})
加个样式
.s-pagination {
display: flex;
/* your component style */
.s-pager {
list-style: none;
display: flex;
padding-left: 0;
li {
margin-top: 0;
padding: 0 4px;
}
li.current {
color: rgb(96, 96, 255);
}
}
}
使用下看看效果:
<template>
<SPagination :total="100"></SPagination>
</template>
只有右更多按钮的效果:
左右更多按钮都有的效果:
只有左更多按钮的效果:
到目前为止,分页器复杂的渲染部分就完成了。
接下来实现相对简单的逻辑部分,直接给各种分页按钮加事件即可。
const { pageIndex, setPageIndex, jumpPage } = usePage()
<ul class="s-pager">
<!-- 首页是常驻的,不管 -->
<li onClick={() => setPageIndex(1)}>1</li>
<!-- 左更多按钮显示的条件有2个: -->
<!-- 1. 总页码totalPage要大于最大页码按钮数量pagerCount -->
<!-- 2. 当前页码大于 Math.ceil(pagerCount / 2) -->
{ totalPage.value > pagerCount && pageIndex.value > Math.ceil(pagerCount / 2) && <li class="more left" onClick={() => jumpPage(-5)}>...</li> }
<!-- 中间页码 -->
{ centerPages.value.map(page => <li onClick={() => setPageIndex(page)}>{page}</li>) }
<!-- 右更多按钮显示的条件也有2个: -->
<!-- 1. 总页码totalPage要大于最大页码按钮数量pagerCount(和左更多按钮相同) -->
<!-- 2. 当前页码小于 totalPage.value - Math.ceil(pagerCount.value / 2) + 1 -->
{ totalPage.value > pagerCount && pageIndex.value < totalPage.value - Math.ceil(pagerCount.value / 2) + 1 && <li class="more right" onClick={() => jumpPage(5)}>...</li> }
<!-- 尾页显示的条件是:总页码大于1 -->
{ totalPage.value > 1 && <li onClick={() => setPageIndex(totalPage.value)}>{totalPage.value}</li>}
</ul>
我们的基础分页组件基本完成,主要实现了以下功能:
- 上一页、下一页
- 分页器,包含:
- 首尾页
- 左右更多按钮(可快速往前、往后跳转N页)
- 中间分页按钮
Pagination组件代码如下:
import { defineComponent, toRefs, computed } from 'vue'
import usePage from './composables/use-page'
import { PaginationProps, paginationProps } from './pagination.type'
import { getCenterPages } from './utils'
export default defineComponent({
name: 'SPagination',
props: paginationProps,
setup(props: PaginationProps) {
const pagerCount = ref(7)
const { total, pageSize } = toRefs(props)
const totalPage = Math.ceil(total.value / pageSize.value)
const { pageIndex, prevPage, nextPage, setPageIndex, jumpPage } = usePage()
const centerPages = computed(() => getCenterPages(totalPage.value, pageIndex.value, pagerCount.value))
return () => (
<div class="s-pagination">
<button onClick={prevPage}>上一页</button>
<!-- Pager 部分 start -->
<ul class="s-pager">
<!-- 首页是常驻的,不管 -->
<li onClick={() => setPageIndex(1)}>1</li>
<!-- 左更多按钮显示的条件有2个: -->
<!-- 1. 总页码totalPage要大于最大页码按钮数量pagerCount -->
<!-- 2. 当前页码大于 Math.ceil(pagerCount / 2) -->
{ totalPage.value > pagerCount && pageIndex.value > Math.ceil(pagerCount / 2) && <li class="more left" onClick={() => jumpPage(-5)}>...</li> }
<!-- 中间页码 -->
{ centerPages.value.map(page => <li onClick={() => setPageIndex(page)}>{page}</li>) }
<!-- 右更多按钮显示的条件也有2个: -->
<!-- 1. 总页码totalPage要大于最大页码按钮数量pagerCount(和左更多按钮相同) -->
<!-- 2. 当前页码小于 totalPage.value - Math.ceil(pagerCount.value / 2) + 1 -->
{ totalPage.value > pagerCount && pageIndex.value < totalPage.value - Math.ceil(pagerCount.value / 2) + 1 && <li class="more right" onClick={() => jumpPage(5)}>...</li> }
<!-- 尾页显示的条件是:总页码大于1 -->
{ totalPage.value > 1 && <li onClick={() => setPageIndex(totalPage.value)}>{totalPage.value}</li>}
</ul>
<!-- Pager 部分 end -->
<button onClick={nextPage}>下一页</button>
</div>
)
}
})
效果如下:
5 抽取Pager组件
这时我们发现Pagination
组件已经有120行左右代码,代码量看起来还是略显得有些多,特别是随着后续功能的增加,setup的代码会越来越多,与其等代码变成“屎山”,不如在它有恶化的苗头时就开始重构,将“战场”及时打扫干净。
最复杂的部分就是中间的Pager
部分:
<ul class="s-pager">
<li onClick={() => setPageIndex(1)}>1</li>
{ totalPage.value > pagerCount && pageIndex.value > Math.ceil(pagerCount / 2) && <li class="more left" onClick={() => jumpPage(-5)}>...</li> }
{ centerPages.value.map(page => <li onClick={() => setPageIndex(page)}>{page}</li>) }
{ totalPage.value > pagerCount && pageIndex.value < totalPage.value - Math.ceil(pagerCount.value / 2) + 1 && <li class="more right" onClick={() => jumpPage(5)}>...</li> }
{ totalPage.value > 1 && <li onClick={() => setPageIndex(totalPage.value)}>{totalPage.value}</li>}
</ul>
这是Pagination
组件的主体部分,它和上一页、下一页是独立的,应该抽取出来。
抽取的方式分成三步:
在components子组件目录中创建一个pager.tsx
的文件,并将Pager
部分的代码拷贝过去。
import { defineComponent } from 'vue'
export default defineComponent({
name: 'SPager',
setup() {
return () => {
return (
<ul class="s-pager">
<li onClick={() => setPageIndex(1)}>1</li>
{
totalPage.value > pagerCount && pageIndex.value > Math.ceil(pagerCount / 2)
&& <li class="more left" onClick={() => jumpPage(-5)}>...</li>
}
{ centerPages.value.map(page => <li onClick={() => setPageIndex(page)}>{page}</li>) }
{
totalPage.value > pagerCount && pageIndex.value < totalPage.value - Math.ceil(pagerCount.value / 2) + 1
&& <li class="more right" onClick={() => jumpPage(5)}>...</li>
}
{
totalPage.value > 1
&& <li onClick={() => setPageIndex(totalPage.value)}>{totalPage.value}</li>
}
</ul>
)
}
}
})
第二步:缺啥补啥
我们发现缺了很多变量和方法,依然是从Pagination
那边拷贝过去,缺啥补啥。
import { defineComponent, toRefs, computed } from 'vue'
import usePage from './composables/use-page'
import { PagerProps, pagerProps } from './pager.type'
import { getCenterPages } from './utils'
export default defineComponent({
name: 'SPager',
props: pagerProps,
setup(props: PagerProps) {
const pagerCount = ref(7)
const { total, pageSize } = toRefs(props)
const totalPage = Math.ceil(total.value / pageSize.value)
const { pageIndex, setPageIndex, jumpPage } = usePage()
const centerPages = computed(() => getCenterPages(totalPage.value, pageIndex.value, pagerCount.value))
return () => {
return (
<ul class="s-pager">
<li onClick={() => setPageIndex(1)}>1</li>
{
totalPage.value > pagerCount && pageIndex.value > Math.ceil(pagerCount / 2)
&& <li class="more left" onClick={() => jumpPage(-5)}>...</li>
}
{ centerPages.value.map(page => <li onClick={() => setPageIndex(page)}>{page}</li>) }
{
totalPage.value > pagerCount && pageIndex.value < totalPage.value - Math.ceil(pagerCount.value / 2) + 1
&& <li class="more right" onClick={() => jumpPage(5)}>...</li>
}
{
totalPage.value > 1
&& <li onClick={() => setPageIndex(totalPage.value)}>{totalPage.value}</li>
}
</ul>
)
}
}
})
需要注意的是,Pager
和Pagination
组件的props和相应的类型是一样的,直接从pagination-types.ts
中取即可,以下是pager-types.ts
的代码:
import { ExtractPropTypes } from 'vue'
import { paginationProps } from '../pagination-type'
export const pagerProps = paginationProps
export type PagerProps = ExtractPropTypes<typeof pagerProps>
第三步:替换
有了Pager
组件,我们先单独使用看看效果。
## 分页器 pager
通过`s-pager`直接使用分页器组件
:::demo
```vue
<template>
When you have few pages
<SPager :total="50"></SPager>
When you have more than 7 pages
<SPager :total="1000"></SPager>
</template>
由于和Pagination
的props一样,所以使用方式也是一样的。
效果如下:
功能和Pagination组件一样,就是少了上一页、下一页的按钮。
确认子组件Pager的功能没问题之后,我们就可以将其用在Pagination中了。
import { defineComponent, toRefs, computed } from 'vue'
import { PaginationProps, paginationProps } from './pagination.type'
import SPager from './components/pager'
export default defineComponent({
name: 'SPagination',
props: paginationProps,
setup(props: PaginationProps) {
const { total, pageSize } = toRefs(props)
return () => (
<div class="s-pagination">
<button onClick={prevPage}>上一页</button>
<SPager total={total.value} pageSize={pageSize.value}></SPager>
<button onClick={nextPage}>下一页</button>
</div>
)
}
})
再次确认功能依然是正常的,这时我们就完成了Pager
的抽离。
回顾下这次重构,我们
- 先是将pagination.tsx中Pager部分的HTML代码都移到了pager.tsx文件中。
- 然后又用缺啥补啥的原则,将相关的变量和方法也一并移过去。
- 最后用新创建的Pager子组件替换掉Pagination中的部分,并完成重构。
重构的好处
将Pager
子组件从Pagination
中抽离出来,有诸多好处,最显而易见的好处就是
Pagination
组件的代码少了一半(40多行降到不到20行)
代码少了,可读性就提升了,一眼就能看懂代码的意图,后续维护起来也方便,20行代码bug几乎无处藏身。
除了这些好处,还有一个比较隐藏的好处,就是组件的灵活性和可扩展性提升了。
假如有一个用户要用我们的分页组件,但是不需要上一页、下一个的按钮,这时我们可能会在props中加一个:showPrevNextBtn
export const paginationProps = {
total: {
type: Number,
default: 0,
},
pageSize: {
type: Number,
default: 10,
},
showPrevNextBtn: {
type: Boolean,
default: true,
}
}
然后在代码里判断这个api是否设置成true,如果设置成true则展示上一个、下一页按钮,否则就隐藏。
<div class="s-pagination">
{ showPrevNextBtn && <button onClick={prevPage}>上一页</button> }
<!-- Pager 部分 -->
{ showPrevNextBtn && <button onClick={nextPage}>下一页</button> }
</div>
该用户使用的时候,则加上这个参数
<template>
<SPagination :total="50" :showPrevNextBtn="false"></SPagination>
</template>
有了Pager子组件,用户不需要上一页、下一页,很简单,直接用Pager就行呀
<template>
<SPager :total="50"></SPager>
</template>
也不用加额外的参数。
6 给Pagination加功能
有了核心的分页能力,并且实现了Pager子组件,要实现各种功能就是加props的问题啦。
左右分页按钮的可用状态逻辑
当页码在第一页时,我们的上一页按钮应该禁用,不可点击;
同理当页码在最后一页时,我们的下一页按钮也应该禁用。
我们先加上这个逻辑:
setup(props: PaginationProps) {
const pager = ref()
// 按钮禁用状态
const disablePrev = computed(() =>
pager.value ? pager.value.pageIndex < 2 : true
)
const disableNext = computed(() =>
pager.value ? pager.value.pageIndex > pager.value.totalPage - 1 : true
)
return () => (
<div class="s-pagination">
<button disabled={disablePrev.value} onClick={prevPage}>上一页</button>
<SPager ref={pager}></SPager>
<button disabled={disableNext.value} onClick={nextPage}>下一页</button>
</div>
)
}
动态改变total和pageSize
目前我们的Pagination组件只有两个参数:
- total 总数据条数,默认值为0
- pageSize 每页数据条数,默认值为10
并且都不是必填的,当什么都不传时,会显示一个高亮的1,并且上一页、下一页按钮都是禁用状态。
我们可以只传total,比如后台返回了10000条数据给前端,因为pageSize默认时10,所以会显示1000页。
假如用户删除了部分数据,这时total是需要动态变化的,目前我们的Pagination组件是会有一个bug的。
还记得我们之前计算totalPage的逻辑吗?之前为了动态控制下一页按钮的可用状态,我们需要比较当前页码和最大页码,所以需要计算totalPage。
const totalPage = Math.ceil(total.value / pageSize.value)
<button disabled={pageIndex.value > totalPage - 1} onClick={nextPage}><IconArrowRight /></button>
由于totalPage是一次性计算的,当total动态变化时,totalPage并不会跟着变化,这会导致下一页按钮不会动态变化,这就出bug啦。
这时我们需要将totalPage的计算逻辑用computed包裹一下,成为一个计算属性:
const totalPage = computed(() => Math.ceil(total.value / pageSize.value))
这时total就能动态变化啦,并且如果我们需要动态改变pageSize的值,比如每页10条数据太少了,想设置每页100条数据,目前也是可以实现的。
设置最大页码按钮数
还记得我们开发Pager时,提到一个pagerCount最大页码按钮数,该变量当时是写死的7,这个变量是用来控制页码按钮数量的,避免显示太多页码按钮,导致页面不美观。
const pagerCount = ref(7)
如果用户觉得这个数字太大或者太小,想调整怎么办呢?
这时我们就需要增加第3页props:pagerCount
修改pagination-type.ts文件:
export const paginationProps = {
total: {
type: Number,
default: 0,
},
pageSize: {
type: Number,
default: 10,
},
pagerCount: {
type: Number,
default: 7
}
};
然后把写死的pagerCount改成从props中获取:
const pagerCount = ref(7)
const { total, pageSize } = toRefs(props)
->
const { total, pageSize, pagerCount } = toRefs(props)
由于用到pagerCount变量的centerPages是computed计算属性,因此pagerCount也是可以动态设置的。
const centerPages = computed(() => getCenterPages(totalPage.value, pageIndex.value, pagerCount.value))
效果如下:
pageIndex双向绑定
开发了那么多功能,到头来我们会发现业务根本用不起来:
因为我们的Pagination组件没有输出
业务使用的时候,关键是需要知道当前页码是什么,目前当前页码状态是封闭在Pagination组件内部的,并没有暴露出去。
我们考虑把pageIndex当前页码做成双向绑定,这样方便业务灵活使用。
这时我们需要增加第4个props:modelValue,以及与之配套的emits:update:modelValue
先在pagination-types.ts文件中增加modelValue
export const paginationProps = {
modelValue: {
type: Number,
default: 1
}
};
再在pagination.tsx中增加update:modelValue
export default defineComponent({
name: 'SPagination',
props: paginationProps,
emits: ['update:modelValue'], // 新增
setup(props: PaginationProps, { emit }) {
...
// 每当内部的 pageIndex 发生变化,都将最新的 pageIndex 值暴露给外部
onMounted(() => {
watch(
() => pager.value.pageIndex,
(newVal: number) => {
emit('update:modelValue', newVal)
}
)
})
...
}
})
效果如下:
这样就够了吗?
这样做只是打通了从组件内到组件外的数据传递,并没有将组件外部的数据同步给组件内部,比如v-model初始值设置为2,这时咱们的Pagination并没有高亮第2页。
因此我们还需要加上从外到内的数据同步:
// modelValue 代表外部数据,当 modelValue 发生变化时,应该同步改变内部的 pageIndex
watch(
() => props.modelValue,
(newVal: number) => {
pager.value.pageIndex = newVal
}
)
到目前为止我们的Pagination组件就完成了,主要包含以下特性:
- 动态设置total / pageSize / pagerCount
- pageIndex支持双向绑定
- 可独立使用的分页器Pager
- UI无关的usePage
- 左右分页
基于现有的核心功能我们可以很轻易地扩展出以下辅助功能和特定样式的分页组件。
比如带背景色的分页:
又比如这种小型分页:
以及各种带附加功能的分页: