> 技术文档 > Base UI分页组件:Pagination的实现方案

Base UI分页组件:Pagination的实现方案


Base UI分页组件:Pagination的实现方案

【免费下载链接】base-ui Base UI is a library of headless (\"unstyled\") React components and low-level hooks. You gain complete control over your app\'s CSS and accessibility features. 【免费下载链接】base-ui 项目地址: https://gitcode.com/GitHub_Trending/ba/base-ui

引言

在现代Web应用中,分页(Pagination)是处理大量数据展示的核心组件。无论是电商平台的产品列表、内容管理系统的文章列表,还是数据分析平台的结果展示,分页组件都扮演着至关重要的角色。Base UI作为无样式(headless)的React组件库,为开发者提供了构建高度定制化分页组件的完美基础。

本文将深入探讨基于Base UI架构的分页组件实现方案,涵盖设计理念、核心功能、API设计和最佳实践。

分页组件的核心需求

功能需求矩阵

功能模块 基础需求 高级需求 无障碍需求 页面导航 上一页/下一页 首页/末页 键盘导航支持 页码显示 当前页码 页码范围 屏幕阅读器支持 数据控制 每页条数 总页数计算 焦点管理 状态管理 当前页面状态 禁用状态 ARIA属性

分页模式对比

mermaid

Base UI分页组件架构设计

核心Hook:usePagination

interface UsePaginationParameters { /** 总数据条数 */ total: number /** 当前页码 */ page?: number /** 每页显示条数 */ pageSize?: number /** 是否禁用 */ disabled?: boolean /** 可见页码数量 */ siblingCount?: number /** 边界页码数量 */ boundaryCount?: number /** 变化回调 */ onChange?: (page: number, pageSize: number) => void}interface UsePaginationReturnValue { /** 分页项数组 */ items: PaginationItem[] /** 当前页码 */ page: number /** 总页数 */ totalPages: number /** 分页属性获取器 */ getPaginationProps: (externalProps?: HTMLProps) => HTMLProps /** 分页项属性获取器 */ getItemProps: (item: PaginationItem, externalProps?: HTMLProps) => HTMLProps}interface PaginationItem { type: \'page\' | \'previous\' | \'next\' | \'first\' | \'last\' | \'ellipsis\' page?: number disabled?: boolean selected?: boolean}

分页算法实现

function generatePaginationItems( currentPage: number, totalPages: number, siblingCount: number = 1, boundaryCount: number = 1): PaginationItem[] { const items: PaginationItem[] = [] // 添加首页按钮 if (boundaryCount > 0 && currentPage > 1) { items.push({ type: \'first\', page: 1 }) } // 添加上一页按钮 if (currentPage > 1) { items.push({ type: \'previous\', page: currentPage - 1 }) } // 计算页码范围 const startPages = Array.from({ length: boundaryCount }, (_, i) => i + 1) const endPages = Array.from({ length: boundaryCount }, (_, i) => totalPages - i).reverse() const siblingsStart = Math.max( boundaryCount + 1, currentPage - siblingCount ) const siblingsEnd = Math.min( totalPages - boundaryCount, currentPage + siblingCount ) // 添加起始页码 items.push(...startPages.map(page => ({ type: \'page\', page, selected: page === currentPage }))) // 添加省略号(如果需要) if (siblingsStart > boundaryCount + 1) { items.push({ type: \'ellipsis\' }) } // 添加中间页码 for (let page = siblingsStart; page  boundaryCount && page <= totalPages - boundaryCount) { items.push({ type: \'page\', page, selected: page === currentPage }) } } // 添加省略号(如果需要) if (siblingsEnd  ({ type: \'page\', page, selected: page === currentPage }))) // 添加下一页按钮 if (currentPage  0 && currentPage < totalPages) { items.push({ type: \'last\', page: totalPages }) } return items}

完整组件实现

Pagination Root组件

import * as React from \'react\'import { usePagination } from \'./usePagination\'import { mergeProps } from \'../merge-props\'interface PaginationRootProps extends UsePaginationParameters { children?: React.ReactNode render?: (props: UsePaginationReturnValue) => React.ReactNode}const PaginationRoot = React.forwardRef( function PaginationRoot(props, ref) { const { total, page: controlledPage, pageSize = 10, disabled = false, siblingCount = 1, boundaryCount = 1, onChange, children, render, ...otherProps } = props const [internalPage, setInternalPage] = React.useState(1) const page = controlledPage ?? internalPage const pagination = usePagination({ total, page, pageSize, disabled, siblingCount, boundaryCount, onChange: (newPage, newPageSize) => { if (!controlledPage) { setInternalPage(newPage) } onChange?.(newPage, newPageSize) } }) const getRootProps = React.useCallback( (externalProps: React.HTMLAttributes = {}) => { return mergeProps( { role: \'navigation\', \'aria-label\': \'分页导航\', ref, }, externalProps, otherProps ) }, [ref, otherProps] ) const contextValue = React.useMemo(() => pagination, [pagination]) if (render) { return render(pagination) as React.ReactElement } return (  
{children}
) })

Pagination Item组件

interface PaginationItemProps { item: PaginationItem children?: React.ReactNode render?: (props: { item: PaginationItem getItemProps: (externalProps?: HTMLProps) => HTMLProps }) => React.ReactNode}const PaginationItem = React.forwardRef( function PaginationItem(props, ref) { const { item, children, render, ...otherProps } = props const pagination = React.useContext(PaginationContext) if (!pagination) { throw new Error(\'PaginationItem must be used within PaginationRoot\') } const handleClick = React.useCallback(() => { if (item.page !== undefined && !item.disabled) { pagination.onChange?.(item.page, pagination.pageSize) } }, [item, pagination]) const getItemProps = React.useCallback( (externalProps: HTMLProps = {}) => { const baseProps: HTMLProps = { \'aria-current\': item.selected ? \'page\' : undefined, \'aria-disabled\': item.disabled || undefined, \'aria-label\': getAriaLabel(item), onClick: handleClick, ref, tabIndex: item.disabled ? -1 : 0, } return mergeProps(baseProps, externalProps, otherProps) }, [item, handleClick, ref, otherProps] ) if (render) { return render({ item, getItemProps }) as React.ReactElement } return (  ) })function getAriaLabel(item: PaginationItem): string { switch (item.type) { case \'first\': return \'跳转到首页\' case \'previous\': return \'跳转到上一页\' case \'next\': return \'跳转到下一页\' case \'last\': return \'跳转到末页\' case \'ellipsis\': return \'更多页码\' case \'page\': return `跳转到第 ${item.page} 页` default: return \'\' }}function getItemContent(item: PaginationItem): React.ReactNode { switch (item.type) { case \'first\': return \'«\' case \'previous\': return \'‹\' case \'next\': return \'›\' case \'last\': return \'»\' case \'ellipsis\': return \'…\' case \'page\': return item.page default: return null }}

使用示例

基础用法

import { PaginationRoot, PaginationItem } from \'@base-ui/react/pagination\'function DataTable({ data, total }) { const [page, setPage] = React.useState(1) const [pageSize, setPageSize] = React.useState(10) return ( 
{/* 数据表格内容 */} { setPage(newPage) setPageSize(newPageSize) }} render={({ items, getPaginationProps }) => ( )} />
)}

高级定制示例

function CustomPagination() { return (  ( 
第 {page} 页,共 {totalPages} 页
{items.map((item, index) => ( { const props = getItemProps() return ( {item.type === \'ellipsis\' ? ( ) : ( getItemContent(item))} ) }} /> ))}
setPageSize(newPageSize)} />
)} /> )}

无障碍访问支持

ARIA属性实现

const getPaginationProps = (externalProps: HTMLProps = {}) => { return mergeProps( { role: \'navigation\', \'aria-label\': \'分页导航\', \'aria-current\': \'page\', }, externalProps )}const getItemProps = (item: PaginationItem, externalProps: HTMLProps = {}) => { const baseProps: HTMLProps = { \'aria-current\': item.selected ? \'page\' : undefined, \'aria-disabled\': item.disabled || undefined, \'aria-label\': getAriaLabel(item), role: item.type === \'ellipsis\' ? \'presentation\' : \'button\', tabIndex: item.disabled ? -1 : 0, } return mergeProps(baseProps, externalProps)}

键盘导航支持

const handleKeyDown = React.useCallback((event: React.KeyboardEvent) => { switch (event.key) { case \'ArrowLeft\': event.preventDefault() // 导航到上一页 break case \'ArrowRight\': event.preventDefault() // 导航到下一页 break case \'Home\': event.preventDefault() // 导航到首页 break case \'End\': event.preventDefault() // 导航到末页 break case \'Enter\': case \'Space\': if (item.type !== \'ellipsis\') { event.preventDefault() handleClick() } break }}, [item, handleClick])

性能优化策略

内存化优化

const paginationItems = React.useMemo(() => { return generatePaginationItems( currentPage, totalPages, siblingCount, boundaryCount )}, [currentPage, totalPages, siblingCount, boundaryCount])const contextValue = React.useMemo(() => ({ items: paginationItems, page: currentPage, totalPages, pageSize, onChange, getPaginationProps, getItemProps,}), [paginationItems, currentPage, totalPages, pageSize, onChange, getPaginationProps, getItemProps])

虚拟化渲染

对于超大数据集的分页,可以采用虚拟化渲染策略:

function VirtualizedPagination({ total, pageSize, ...props }) { const visibleItems = React.useMemo(() => { // 只渲染当前可见范围内的分页项 return generateVisiblePaginationItems(currentPage, totalPages) }, [currentPage, totalPages]) return (  (  )} {...props} /> )}

测试策略

单元测试示例

import { render, screen, fireEvent } from \'@testing-library/react\'import { usePagination } from \'./usePagination\'describe(\'usePagination\', () => { it(\'应该正确生成分页项\', () => { const TestComponent = () => { const { items } = usePagination({ total: 100, pageSize: 10 }) return ( 
{items.map((item, index) => ( {item.type} ))}
) } render() expect(screen.getByTestId(\'item-0\')).toHaveTextContent(\'page\') }) it(\'应该正确处理页码变化\', async () => { const onChange = jest.fn() const TestComponent = () => { const { getItemProps } = usePagination({ total: 100, pageSize: 10, onChange, }) return ( ) } render() fireEvent.click(screen.getByTestId(\'page-2\')) expect(onChange).toHaveBeenCalledWith(2, 10) })})

【免费下载链接】base-ui Base UI is a library of headless (\"unstyled\") React components and low-level hooks. You gain complete control over your app\'s CSS and accessibility features. 【免费下载链接】base-ui 项目地址: https://gitcode.com/GitHub_Trending/ba/base-ui

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考