前端分页组件实战踩坑记性能优化与用户体验双管齐下

秀丽的笔记 组件 阅读 713
赞 16 收藏
二维码
手机扫码查看
反馈

先说说我为什么写这篇文章

最近做项目又遇到分页组件了,说实话,这玩意儿看起来简单,但真要做得用户友好还是有不少坑的。之前我都是直接拿现成的UI库里的分页,但这次产品提了一些特殊需求,比如需要显示当前页码范围、自定义每页条数等等,搞了一圈发现还是得自己写一个比较灵活。

前端分页组件实战踩坑记性能优化与用户体验双管齐下

写完之后觉得有必要记录一下,毕竟分页这东西太常见了,每次都要重新造轮子确实麻烦。

基础版本,先跑起来再说

最基本的分页组件,其实逻辑很简单:计算总页数、生成页码按钮、处理点击事件。我先写个最简单的版本:

function Pagination({ 
    total,        // 总数据条数
    current,      // 当前页码
    pageSize,     // 每页条数
    onChange      // 页码变化回调
}) {
    const totalPages = Math.ceil(total / pageSize);
    
    // 生成页码数组
    function generatePages() {
        if (totalPages <= 7) {
            return Array.from({ length: totalPages }, (_, i) => i + 1);
        }
        
        const pages = [];
        // 总是显示第一页
        pages.push(1);
        
        // 显示省略号的情况
        if (current > 4) {
            pages.push('...');
        }
        
        // 显示当前页附近的页码
        const start = Math.max(2, current - 2);
        const end = Math.min(totalPages - 1, current + 2);
        
        for (let i = start; i <= end; i++) {
            pages.push(i);
        }
        
        if (current < totalPages - 3) {
            pages.push('...');
        }
        
        // 总是显示最后一页
        if (totalPages > 1) {
            pages.push(totalPages);
        }
        
        return pages;
    }
    
    return (
        <div className="pagination">
            <button 
                onClick={() => onChange(Math.max(1, current - 1))}
                disabled={current === 1}
            >
                上一页
            </button>
            
            {generatePages().map((page, index) => (
                <button
                    key={index}
                    onClick={() => typeof page === 'number' && onChange(page)}
                    className={page === current ? 'active' : ''}
                    disabled={typeof page !== 'number'}
                >
                    {page}
                </button>
            ))}
            
            <button 
                onClick={() => onChange(Math.min(totalPages, current + 1))}
                disabled={current === totalPages}
            >
                下一页
            </button>
        </div>
    );
}

样式部分,看着舒服就行

CSS这块没啥特别的,主要是让分页按钮居中显示,当前页高亮:

.pagination {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    margin: 20px 0;
}

.pagination button {
    padding: 8px 12px;
    border: 1px solid #ddd;
    background: white;
    cursor: pointer;
    border-radius: 4px;
    transition: all 0.2s;
}

.pagination button:hover:not(:disabled) {
    background: #f5f5f5;
}

.pagination button.active {
    background: #007bff;
    color: white;
    border-color: #007bff;
}

.pagination button:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

踩坑提醒:这几点一定注意

边界条件处理:这是最容易出错的地方。比如总共只有1页,或者当前页超出范围。我在测试的时候就发现,当总页数小于等于7时,原来的逻辑会导致重复渲染某些页码。所以我在generatePages函数里加了个判断,只有总页数大于7才走复杂的省略逻辑。

页码输入验证:如果用户手动输入页码,一定要做验证:

// 验证页码有效性
function validatePage(page, totalPages) {
    const num = parseInt(page);
    if (isNaN(num) || num < 1 || num > totalPages) {
        return 1; // 返回默认值
    }
    return num;
}

防抖处理:用户可能会疯狂点击页码按钮,这时候需要防抖:

import { debounce } from 'lodash';

const debouncedChange = debounce((page) => {
    onChange(page);
}, 300);

// 在按钮点击事件中使用
onClick={() => typeof page === 'number' && debouncedChange(page)}

高级功能,让产品满意

刚才那个基础版本只能满足基本需求,产品经常会问能不能显示总数、能不能自定义每页条数。这些功能加起来也不是很复杂:

function AdvancedPagination({
    total,
    current,
    pageSize = 10,
    pageSizeOptions = [10, 20, 50, 100],
    showQuickJumper = false,
    showSizeChanger = false,
    showTotal = true,
    onChange,
    onPageSizeChange
}) {
    const totalPages = Math.ceil(total / pageSize);
    
    // 显示总数据信息
    const totalInfo = showTotal && (
        <span className="pagination-total">
            共 {total} 条,第 {current} / {totalPages} 页
        </span>
    );
    
    // 页数选择器
    const sizeSelector = showSizeChanger && (
        <select 
            value={pageSize} 
            onChange={(e) => onPageSizeChange?.(parseInt(e.target.value))}
            className="pagination-size"
        >
            {pageSizeOptions.map(size => (
                <option key={size} value={size}>{size} 条/页</option>
            ))}
        </select>
    );
    
    // 快速跳转
    const quickJumper = showQuickJumper && (
        <span className="pagination-jumper">
            跳至
            <input
                type="number"
                min="1"
                max={totalPages}
                defaultValue={current}
                onKeyDown={(e) => {
                    if (e.key === 'Enter') {
                        const targetPage = parseInt(e.target.value);
                        if (targetPage >= 1 && targetPage <= totalPages) {
                            onChange(targetPage);
                        }
                    }
                }}
            />
            页
        </span>
    );
    
    return (
        <div className="advanced-pagination">
            {totalInfo}
            <div className="pagination-controls">
                {/* 前面的基础分页组件 */}
            </div>
            {sizeSelector}
            {quickJumper}
        </div>
    );
}

性能优化的一些想法

大部分情况下,分页数据都是通过API获取的,这时候要考虑loading状态和错误处理:

function usePagination(apiCall, initialParams = {}) {
    const [data, setData] = useState([]);
    const [loading, setLoading] = useState(false);
    const [error, setError] = useState(null);
    const [pagination, setPagination] = useState({
        current: 1,
        pageSize: 10,
        total: 0
    });
    
    const fetchData = async (params = {}) => {
        setLoading(true);
        try {
            const result = await apiCall({
                page: params.current || pagination.current,
                limit: params.pageSize || pagination.pageSize,
                ...initialParams
            });
            
            setData(result.data);
            setPagination(prev => ({
                ...prev,
                ...params,
                total: result.total
            }));
            setError(null);
        } catch (err) {
            setError(err.message);
        } finally {
            setLoading(false);
        }
    };
    
    useEffect(() => {
        fetchData();
    }, [JSON.stringify(initialParams)]);
    
    return {
        data,
        loading,
        error,
        pagination,
        onChange: (current, pageSize) => fetchData({ current, pageSize }),
        refresh: () => fetchData()
    };
}

这样封装后,在业务组件中就可以这么用了:

function ProductList() {
    const { data, loading, error, pagination, onChange } = usePagination(
        (params) => fetch('/api/products', params)
    );
    
    if (loading) return <div>加载中...</div>;
    if (error) return <div>错误:{error}</div>;
    
    return (
        <div>
            <ProductTable data={data} />
            <AdvancedPagination
                total={pagination.total}
                current={pagination.current}
                pageSize={pagination.pageSize}
                onChange={onChange}
            />
        </div>
    );
}

实际项目中的使用感受

我在实际项目中用下来,发现几个需要注意的地方:

  • 移动端的分页按钮要适当增大,方便点击
  • 对于大数据量的列表,建议加上虚拟滚动,不然页面会卡
  • 如果列表支持筛选,记得在筛选条件改变时重置页码为1
  • 分页组件的状态管理如果比较复杂,建议用Redux或Context来统一管理

还有个小技巧,有时候后端返回的数据格式不太一样,可以在请求的时候做个适配器:

async function fetchProducts(params) {
    const response = await fetch(https://jztheme.com/api/products, {
        method: 'POST',
        body: JSON.stringify(params)
    });
    
    const result = await response.json();
    
    // 适配不同后端返回格式
    return {
        data: result.items || result.data,
        total: result.count || result.total || result.totalCount
    };
}

一些特殊场景的处理

有些业务场景比较特殊,比如表格组件内部的分页,可能需要和表格的状态联动。这时候建议把分页作为一个独立的子组件,通过props传递状态:

function TableWithPagination({ columns, dataSource, pagination }) {
    return (
        <div className="table-container">
            <table>
                {/* 表格内容 */}
            </table>
            <Pagination {...pagination} />
        </div>
    );
}

这样既保持了组件的独立性,又能在需要的时候轻松替换分页逻辑。

最后的思考

分页组件看似简单,但要做得好用还是有很多细节要考虑。特别是用户体验方面,比如页码过多时的省略策略、快速跳转的便捷性、当前页的视觉反馈等等。

我觉得一个好的分页组件应该具备这几个特点:易用、稳定、可定制、性能好。当然,具体怎么实现还是要根据项目需求来定,有时候简单粗暴的实现就够了,没必要一开始就搞得很复杂。

以上是我踩坑后的总结,希望能对你有帮助。这个技巧的拓展用法还有很多,后续会继续分享这类博客。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论