引言
在现代Web开发中,分页功能是数据展示的核心组件之一。无论是电商网站的商品列表、新闻网站的文章列表,还是后台管理系统的数据表格,分页都能有效提升用户体验和页面性能。本文将从零开始,详细讲解如何使用纯JavaScript实现前端分页效果,并提供常见问题的解决方案。
一、分页的基本概念
1.1 什么是分页?
分页是将大量数据分成若干页进行展示的技术。用户可以通过点击页码、上一页、下一页等控件来浏览不同页面的数据。
1.2 分页的核心要素
总数据量
:需要展示的全部数据条数
每页数据量
:每页显示的数据条数
当前页码
:用户当前查看的页面
总页数
:根据总数据量和每页数据量计算得出
分页控件
:页码按钮、上一页/下一页按钮等
二、基础实现:纯JavaScript分页
2.1 HTML结构准备
首先,我们需要准备基础的HTML结构:
JavaScript分页示例
2.2 JavaScript核心逻辑
创建一个名为
pagination.js
的文件,实现分页逻辑:
// pagination.js
// 模拟数据生成器 – 生成100条测试数据
function generateMockData(count = 100) {
const data = [];
for (let i = 1; i <= count; i++) {
data.push({
id: i,
name: `项目 ${i}`,
description: `这是第 ${i} 个项目的详细描述`,
date: new Date(2023, 0, i).toLocaleDateString('zh-CN'),
status: i % 3 === 0 ? '已完成' : (i % 3 === 1 ? '进行中' : '待处理')
});
}
return data;
}
// 分页类
class Pagination {
constructor(options = {}) {
// 默认配置
this.config = {
data: [], // 所有数据
pageSize: 10, // 每页显示数量
currentPage: 1, // 当前页码
containerId: 'dataContainer', // 数据容器ID
paginationId: 'pagination', // 分页控件容器ID
pageInfoId: 'pageInfo', // 页面信息容器ID
...options
};
// 初始化
this.init();
}
// 初始化方法
init() {
this.validateConfig();
this.render();
}
// 验证配置
validateConfig() {
if (!this.config.data || !Array.isArray(this.config.data)) {
throw new Error('数据必须是数组');
}
if (this.config.pageSize <= 0) {
throw new Error('每页数量必须大于0');
}
if (this.config.currentPage <= 0) {
throw new Error('当前页码必须大于0');
}
}
// 获取总页数
getTotalPages() {
return Math.ceil(this.config.data.length / this.config.pageSize);
}
// 获取当前页数据
getCurrentPageData() {
const start = (this.config.currentPage - 1) * this.config.pageSize;
const end = start + this.config.pageSize;
return this.config.data.slice(start, end);
}
// 渲染数据
renderData() {
const container = document.getElementById(this.config.containerId);
if (!container) return;
const data = this.getCurrentPageData();
if (data.length === 0) {
container.innerHTML = '
‘;
return;
}
let html = ”;
data.forEach(item => {
html += `
${item.status}
`;
});
container.innerHTML = html;
}
// 渲染分页控件
renderPagination() {
const container = document.getElementById(this.config.paginationId);
if (!container) return;
const totalPages = this.getTotalPages();
if (totalPages <= 1) { container.innerHTML = ''; return; } let html = ''; // 上一页按钮 const prevDisabled = this.config.currentPage === 1 ? 'disabled' : ''; html += ``;
// 页码按钮
const maxButtons = 5; // 最多显示5个页码按钮
let startPage = Math.max(1, this.config.currentPage – Math.floor(maxButtons / 2));
let endPage = Math.min(totalPages, startPage + maxButtons – 1);
// 调整起始页码,确保显示足够的页码
if (endPage – startPage + 1 < maxButtons) {
startPage = Math.max(1, endPage - maxButtons + 1);
}
// 如果不是从第一页开始,显示第一页和省略号
if (startPage > 1) {
html += ``;
if (startPage > 2) {
html += `…`;
}
}
// 页码按钮
for (let i = startPage; i <= endPage; i++) {
const activeClass = i === this.config.currentPage ? 'active' : '';
html += ``;
}
// 如果不是到最后一页,显示省略号和最后一页
if (endPage < totalPages) {
if (endPage < totalPages - 1) {
html += `…`;
}
html += ``;
}
// 下一页按钮
const nextDisabled = this.config.currentPage === totalPages ? ‘disabled’ : ”;
html += ``;
container.innerHTML = html;
// 为按钮添加事件监听
this.addPaginationEventListeners();
}
// 渲染页面信息
renderPageInfo() {
const container = document.getElementById(this.config.pageInfoId);
if (!container) return;
const totalPages = this.getTotalPages();
const start = (this.config.currentPage – 1) * this.config.pageSize + 1;
const end = Math.min(this.config.currentPage * this.config.pageSize, this.config.data.length);
container.textContent = `显示 ${start}-${end} 条,共 ${this.config.data.length} 条数据,第 ${this.config.currentPage}/${totalPages} 页`;
}
// 添加分页事件监听
addPaginationEventListeners() {
const container = document.getElementById(this.config.paginationId);
// 页码点击事件
container.querySelectorAll(‘button[data-page]’).forEach(btn => {
btn.addEventListener(‘click’, (e) => {
const page = parseInt(e.target.dataset.page);
this.goToPage(page);
});
});
// 上一页/下一页事件
const prevBtn = document.getElementById(‘prevPage’);
const nextBtn = document.getElementById(‘nextPage’);
if (prevBtn) {
prevBtn.addEventListener(‘click’, () => this.prevPage());
}
if (nextBtn) {
nextBtn.addEventListener(‘click’, () => this.nextPage());
}
}
// 跳转到指定页
goToPage(page) {
const totalPages = this.getTotalPages();
if (page < 1 || page > totalPages) {
console.warn(`页码 ${page} 超出范围 (1-${totalPages})`);
return;
}
this.config.currentPage = page;
this.render();
}
// 上一页
prevPage() {
if (this.config.currentPage > 1) {
this.goToPage(this.config.currentPage – 1);
}
}
// 下一页
nextPage() {
const totalPages = this.getTotalPages();
if (this.config.currentPage < totalPages) {
this.goToPage(this.config.currentPage + 1);
}
}
// 更新数据
updateData(newData) {
if (!Array.isArray(newData)) {
throw new Error('新数据必须是数组');
}
this.config.data = newData;
this.config.currentPage = 1; // 重置到第一页
this.render();
}
// 更新每页数量
updatePageSize(newSize) {
if (newSize <= 0) {
throw new Error('每页数量必须大于0');
}
this.config.pageSize = newSize;
this.config.currentPage = 1; // 重置到第一页
this.render();
}
// 渲染所有内容
render() {
this.renderData();
this.renderPagination();
this.renderPageInfo();
}
}
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function() {
// 生成测试数据
const mockData = generateMockData(100);
// 创建分页实例
const pagination = new Pagination({
data: mockData,
pageSize: 10,
currentPage: 1
});
// 为应用设置按钮添加事件
const applyBtn = document.getElementById('applySettings');
if (applyBtn) {
applyBtn.addEventListener('click', function() {
const pageSizeInput = document.getElementById('pageSize');
const newSize = parseInt(pageSizeInput.value);
if (newSize > 0 && newSize <= 50) {
pagination.updatePageSize(newSize);
} else {
alert('请输入1-50之间的数字');
}
});
}
// 将分页实例暴露到全局,方便调试
window.pagination = pagination;
});
三、进阶功能实现
3.1 添加搜索功能
在实际应用中,我们经常需要结合搜索功能。以下是增强版的分页类,支持搜索:
// enhanced-pagination.js
class EnhancedPagination extends Pagination {
constructor(options = {}) {
super(options);
// 搜索相关配置
this.searchConfig = {
searchTerm: '',
searchFields: ['name', 'description'], // 搜索字段
...options.searchConfig
};
// 原始数据备份(用于搜索)
this.originalData = [...this.config.data];
}
// 搜索方法
search(term) {
this.searchConfig.searchTerm = term.toLowerCase().trim();
if (!term) {
// 如果搜索词为空,恢复原始数据
this.config.data = [...this.originalData];
} else {
// 执行搜索
this.config.data = this.originalData.filter(item => {
return this.searchConfig.searchFields.some(field => {
const value = item[field];
if (value) {
return value.toString().toLowerCase().includes(this.searchConfig.searchTerm);
}
return false;
});
});
}
// 重置到第一页
this.config.currentPage = 1;
this.render();
}
// 重写更新数据方法,同时更新原始数据
updateData(newData) {
super.updateData(newData);
this.originalData = […newData];
}
}
// 使用示例
document.addEventListener(‘DOMContentLoaded’, function() {
const mockData = generateMockData(100);
const enhancedPagination = new EnhancedPagination({
data: mockData,
pageSize: 10,
searchConfig: {
searchFields: [‘name’, ‘description’, ‘status’]
}
});
// 添加搜索框
const searchContainer = document.createElement(‘div’);
searchContainer.className = ‘controls’;
searchContainer.innerHTML = `
`;
// 插入到页面顶部
document.body.insertBefore(searchContainer, document.querySelector(‘.controls’));
// 搜索事件
document.getElementById(‘searchBtn’).addEventListener(‘click’, function() {
const term = document.getElementById(‘searchInput’).value;
enhancedPagination.search(term);
});
document.getElementById(‘searchInput’).addEventListener(‘keypress’, function(e) {
if (e.key === ‘Enter’) {
const term = this.value;
enhancedPagination.search(term);
}
});
document.getElementById(‘clearSearchBtn’).addEventListener(‘click’, function() {
document.getElementById(‘searchInput’).value = ”;
enhancedPagination.search(”);
});
window.enhancedPagination = enhancedPagination;
});
3.2 添加排序功能
// sortable-pagination.js
class SortablePagination extends EnhancedPagination {
constructor(options = {}) {
super(options);
// 排序配置
this.sortConfig = {
field: null,
direction: ‘asc’, // ‘asc’ 或 ‘desc’
…options.sortConfig
};
}
// 排序方法
sort(field, direction = ‘asc’) {
// 如果点击的是当前排序字段,则切换方向
if (this.sortConfig.field === field) {
this.sortConfig.direction = this.sortConfig.direction === ‘asc’ ? ‘desc’ : ‘asc’;
} else {
this.sortConfig.field = field;
this.sortConfig.direction = direction;
}
// 执行排序
this.config.data.sort((a, b) => {
let valA = a[field];
let valB = b[field];
// 处理数字类型
if (typeof valA === ‘number’ && typeof valB === ‘number’) {
return this.sortConfig.direction === ‘asc’ ? valA – valB : valB – valA;
}
// 处理字符串类型
valA = valA.toString().toLowerCase();
valB = valB.toString().toLowerCase();
if (valA < valB) {
return this.sortConfig.direction === 'asc' ? -1 : 1;
}
if (valA > valB) {
return this.sortConfig.direction === ‘asc’ ? 1 : -1;
}
return 0;
});
this.render();
}
// 重写渲染数据方法,添加排序指示器
renderData() {
const container = document.getElementById(this.config.containerId);
if (!container) return;
const data = this.getCurrentPageData();
if (data.length === 0) {
container.innerHTML = ‘
‘;
return;
}
let html = ”;
data.forEach(item => {
html += `
${item.status}
`;
});
container.innerHTML = html;
}
// 重写渲染分页控件,添加排序控件
renderPagination() {
super.renderPagination();
// 添加排序控件到分页容器上方
const paginationContainer = document.getElementById(this.config.paginationId);
if (!paginationContainer) return;
// 检查是否已存在排序控件
let sortControls = document.getElementById(‘sortControls’);
if (!sortControls) {
sortControls = document.createElement(‘div’);
sortControls.id = ‘sortControls’;
sortControls.style.cssText = ‘display:flex; gap:10px; margin-bottom:10px; justify-content:center; flex-wrap:wrap;’;
paginationContainer.parentNode.insertBefore(sortControls, paginationContainer);
}
// 生成排序按钮
const fields = [‘id’, ‘name’, ‘date’, ‘status’];
let sortHtml = ‘排序:‘;
fields.forEach(field => {
const isActive = this.sortConfig.field === field;
const direction = isActive ? this.sortConfig.direction : ‘asc’;
const arrow = isActive ? (direction === ‘asc’ ? ‘↑’ : ‘↓’) : ”;
sortHtml += `
`;
});
sortControls.innerHTML = sortHtml;
// 添加排序事件
sortControls.querySelectorAll(‘button[data-sort-field]’).forEach(btn => {
btn.addEventListener(‘click’, (e) => {
const field = e.target.dataset.sortField;
const direction = e.target.dataset.sortDirection;
this.sort(field, direction);
});
});
}
}
// 使用示例
document.addEventListener(‘DOMContentLoaded’, function() {
const mockData = generateMockData(100);
const sortablePagination = new SortablePagination({
data: mockData,
pageSize: 10,
searchConfig: {
searchFields: [‘name’, ‘description’, ‘status’]
}
});
window.sortablePagination = sortablePagination;
});
四、常见问题与解决方案
4.1 性能问题
问题描述
:当数据量非常大(如10万+条)时,前端分页可能导致页面卡顿。
解决方案
:
虚拟滚动
:只渲染可视区域的数据
懒加载
:分页时只加载当前页数据
Web Workers
:将数据处理放到后台线程
// 虚拟滚动分页示例
class VirtualScrollPagination {
constructor(options = {}) {
this.config = {
data: [],
pageSize: 20,
containerHeight: 400, // 容器高度
itemHeight: 50, // 每项高度
…options
};
this.visibleStart = 0;
this.visibleEnd = 0;
this.scrollTop = 0;
this.init();
}
init() {
this.createContainer();
this.bindScrollEvent();
this.updateVisibleRange();
}
createContainer() {
const container = document.getElementById(this.config.containerId);
if (!container) return;
// 设置容器高度
container.style.height = this.config.containerHeight + ‘px’;
container.style.overflowY = ‘auto’;
container.style.position = ‘relative’;
// 创建占位元素
const placeholder = document.createElement(‘div’);
placeholder.id = ‘virtualPlaceholder’;
placeholder.style.height = (this.config.data.length * this.config.itemHeight) + ‘px’;
container.appendChild(placeholder);
// 创建可视区域容器
const viewport = document.createElement(‘div’);
viewport.id = ‘virtualViewport’;
viewport.style.position = ‘absolute’;
viewport.style.top = ‘0’;
viewport.style.left = ‘0’;
viewport.style.right = ‘0’;
container.appendChild(viewport);
}
bindScrollEvent() {
const container = document.getElementById(this.config.containerId);
if (!container) return;
container.addEventListener(‘scroll’, () => {
this.scrollTop = container.scrollTop;
this.updateVisibleRange();
});
}
updateVisibleRange() {
const containerHeight = this.config.containerHeight;
const itemHeight = this.config.itemHeight;
// 计算可见范围
const start = Math.floor(this.scrollTop / itemHeight);
const end = Math.ceil((this.scrollTop + containerHeight) / itemHeight);
// 添加缓冲区
const buffer = 5;
this.visibleStart = Math.max(0, start – buffer);
this.visibleEnd = Math.min(this.config.data.length, end + buffer);
this.renderVisibleItems();
}
renderVisibleItems() {
const viewport = document.getElementById(‘virtualViewport’);
if (!viewport) return;
// 清空现有内容
viewport.innerHTML = ”;
// 设置偏移
viewport.style.top = (this.visibleStart * this.config.itemHeight) + ‘px’;
// 渲染可见项
for (let i = this.visibleStart; i < this.visibleEnd; i++) {
const item = this.config.data[i];
const div = document.createElement('div');
div.style.height = this.config.itemHeight + 'px';
div.style.borderBottom = '1px solid #eee';
div.style.padding = '10px';
div.style.boxSizing = 'border-box';
div.textContent = `${item.name} - ${item.description}`;
viewport.appendChild(div);
}
}
// 更新数据
updateData(newData) {
this.config.data = newData;
// 更新占位元素高度
const placeholder = document.getElementById('virtualPlaceholder');
if (placeholder) {
placeholder.style.height = (this.config.data.length * this.config.itemHeight) + 'px';
}
this.updateVisibleRange();
}
}
4.2 内存泄漏问题
问题描述
:频繁创建和销毁DOM元素可能导致内存泄漏。
解决方案
:
事件委托
:使用事件委托减少事件监听器数量
对象复用
:复用DOM元素而非频繁创建
清理事件监听器
:在不需要时移除事件监听器
// 优化后的事件处理
class OptimizedPagination extends Pagination {
constructor(options = {}) {
super(options);
// 使用事件委托
this.setupEventDelegation();
}
setupEventDelegation() {
const paginationContainer = document.getElementById(this.config.paginationId);
if (!paginationContainer) return;
// 使用事件委托处理所有分页按钮点击
paginationContainer.addEventListener('click', (e) => {
const target = e.target;
if (target.tagName === ‘BUTTON’) {
// 上一页/下一页
if (target.id === ‘prevPage’) {
this.prevPage();
} else if (target.id === ‘nextPage’) {
this.nextPage();
}
// 页码按钮
else if (target.dataset.page) {
const page = parseInt(target.dataset.page);
this.goToPage(page);
}
}
});
}
// 重写渲染方法,避免重复创建事件监听器
renderPagination() {
const container = document.getElementById(this.config.paginationId);
if (!container) return;
const totalPages = this.getTotalPages();
if (totalPages <= 1) {
container.innerHTML = '';
return;
}
// 使用DocumentFragment减少DOM操作
const fragment = document.createDocumentFragment();
// 上一页按钮
const prevBtn = document.createElement('button');
prevBtn.id = 'prevPage';
prevBtn.textContent = '上一页';
if (this.config.currentPage === 1) {
prevBtn.disabled = true;
}
fragment.appendChild(prevBtn);
// 页码按钮逻辑(与之前相同,但使用fragment)
const maxButtons = 5;
let startPage = Math.max(1, this.config.currentPage - Math.floor(maxButtons / 2));
let endPage = Math.min(totalPages, startPage + maxButtons - 1);
if (endPage - startPage + 1 < maxButtons) {
startPage = Math.max(1, endPage - maxButtons + 1);
}
if (startPage > 1) {
const firstBtn = document.createElement(‘button’);
firstBtn.dataset.page = ‘1’;
firstBtn.textContent = ‘1’;
fragment.appendChild(firstBtn);
if (startPage > 2) {
const ellipsis = document.createElement(‘span’);
ellipsis.textContent = ‘…’;
ellipsis.style.padding = ‘8px’;
fragment.appendChild(ellipsis);
}
}
for (let i = startPage; i <= endPage; i++) {
const btn = document.createElement('button');
btn.dataset.page = i;
btn.textContent = i;
if (i === this.config.currentPage) {
btn.classList.add('active');
}
fragment.appendChild(btn);
}
if (endPage < totalPages) {
if (endPage < totalPages - 1) {
const ellipsis = document.createElement('span');
ellipsis.textContent = '...';
ellipsis.style.padding = '8px';
fragment.appendChild(ellipsis);
}
const lastBtn = document.createElement('button');
lastBtn.dataset.page = totalPages;
lastBtn.textContent = totalPages;
fragment.appendChild(lastBtn);
}
// 下一页按钮
const nextBtn = document.createElement('button');
nextBtn.id = 'nextPage';
nextBtn.textContent = '下一页';
if (this.config.currentPage === totalPages) {
nextBtn.disabled = true;
}
fragment.appendChild(nextBtn);
// 一次性更新DOM
container.innerHTML = '';
container.appendChild(fragment);
}
}
4.3 响应式设计问题
问题描述
:在移动设备上分页控件显示不全或布局错乱。
解决方案
:
/* 响应式分页样式 */
@media (max-width: 768px) {
.pagination {
gap: 3px;
}
.pagination button {
padding: 6px 8px;
font-size: 12px;
}
.pagination span {
padding: 6px;
}
/* 在小屏幕上只显示当前页和相邻页码 */
.pagination button[data-page] {
display: none;
}
.pagination button[data-page].active,
.pagination button[data-page].active + button[data-page],
.pagination button[data-page].active ~ button[data-page]:first-of-type {
display: inline-block;
}
/* 上一页/下一页按钮始终显示 */
.pagination button#prevPage,
.pagination button#nextPage {
display: inline-block;
}
}
@media (max-width: 480px) {
.controls {
flex-direction: column;
align-items: stretch;
}
.controls input {
width: 100%;
}
.pagination {
flex-wrap: wrap;
}
/* 移动端优化:减少页码显示数量 */
.pagination button[data-page] {
display: none;
}
.pagination button[data-page].active {
display: inline-block;
}
/* 显示省略号和最后一页 */
.pagination span,
.pagination button[data-page]:last-child {
display: inline-block;
}
}
4.4 数据更新问题
问题描述
:当数据动态变化时,分页状态可能不正确。
解决方案
:
// 数据管理器
class DataManager {
constructor() {
this.data = [];
this.subscribers = [];
}
// 订阅数据变化
subscribe(callback) {
this.subscribers.push(callback);
}
// 通知订阅者
notify() {
this.subscribers.forEach(callback => callback(this.data));
}
// 更新数据
updateData(newData) {
this.data = newData;
this.notify();
}
// 添加数据
addData(item) {
this.data.push(item);
this.notify();
}
// 删除数据
removeData(id) {
this.data = this.data.filter(item => item.id !== id);
this.notify();
}
}
// 使用数据管理器的分页
class DataAwarePagination extends Pagination {
constructor(options = {}) {
super(options);
this.dataManager = options.dataManager || new DataManager();
this.dataManager.subscribe((newData) => {
this.updateData(newData);
});
}
// 重写更新数据方法
updateData(newData) {
// 保存当前页码
const currentPage = this.config.currentPage;
// 更新数据
super.updateData(newData);
// 尝试保持在当前页
const totalPages = this.getTotalPages();
if (currentPage <= totalPages) {
this.config.currentPage = currentPage;
this.render();
}
}
}
五、完整示例与最佳实践
5.1 完整的分页组件
// complete-pagination.js
class CompletePagination {
constructor(options = {}) {
// 默认配置
this.config = {
data: [],
pageSize: 10,
currentPage: 1,
containerId: 'pagination-container',
showSearch: true,
showSort: true,
showPageSizeSelector: true,
maxVisiblePages: 5,
...options
};
// 状态管理
this.state = {
searchTerm: '',
sortField: null,
sortDirection: 'asc',
filteredData: []
};
// DOM缓存
this.dom = {};
this.init();
}
init() {
this.validateConfig();
this.setupDOM();
this.bindEvents();
this.render();
}
validateConfig() {
if (!this.config.data || !Array.isArray(this.config.data)) {
throw new Error('数据必须是数组');
}
if (this.config.pageSize <= 0) {
throw new Error('每页数量必须大于0');
}
}
setupDOM() {
const container = document.getElementById(this.config.containerId);
if (!container) {
throw new Error(`容器元素 #${this.config.containerId} 不存在`);
}
// 创建HTML结构
container.innerHTML = `
` : ”}
${this.config.showPageSizeSelector ? `
` : ”}
` : ”}
`;
// 缓存DOM元素
this.dom.container = container;
this.dom.dataContainer = container.querySelector(‘#data-container’);
this.dom.infoContainer = container.querySelector(‘#pagination-info’);
this.dom.buttonsContainer = container.querySelector(‘#pagination-buttons’);
if (this.config.showSearch) {
this.dom.searchInput = container.querySelector(‘.search-input’);
this.dom.searchBtn = container.querySelector(‘.search-btn’);
this.dom.clearBtn = container.querySelector(‘.clear-btn’);
}
if (this.config.showPageSizeSelector) {
this.dom.pageSizeSelect = container.querySelector(‘.page-size-select’);
}
if (this.config.showSort) {
this.dom.sortButtons = container.querySelectorAll(‘.sort-btn’);
}
}
bindEvents() {
// 搜索事件
if (this.dom.searchBtn) {
this.dom.searchBtn.addEventListener(‘click’, () => this.handleSearch());
}
if (this.dom.searchInput) {
this.dom.searchInput.addEventListener(‘keypress’, (e) => {
if (e.key === ‘Enter’) this.handleSearch();
});
}
if (this.dom.clearBtn) {
this.dom.clearBtn.addEventListener(‘click’, () => this.handleClearSearch());
}
// 每页数量选择事件
if (this.dom.pageSizeSelect) {
this.dom.pageSizeSelect.addEventListener(‘change’, (e) => {
this.config.pageSize = parseInt(e.target.value);
this.config.currentPage = 1;
this.render();
});
}
// 排序事件
if (this.dom.sortButtons) {
this.dom.sortButtons.forEach(btn => {
btn.addEventListener(‘click’, (e) => {
const field = e.target.dataset.field;
this.handleSort(field);
});
});
}
// 分页按钮事件(使用事件委托)
if (this.dom.buttonsContainer) {
this.dom.buttonsContainer.addEventListener(‘click’, (e) => {
const target = e.target;
if (target.tagName === ‘BUTTON’) {
if (target.id === ‘prev-btn’) {
this.prevPage();
} else if (target.id === ‘next-btn’) {
this.nextPage();
} else if (target.dataset.page) {
const page = parseInt(target.dataset.page);
this.goToPage(page);
}
}
});
}
}
handleSearch() {
const term = this.dom.searchInput.value.trim().toLowerCase();
this.state.searchTerm = term;
this.config.currentPage = 1;
this.render();
}
handleClearSearch() {
this.dom.searchInput.value = ”;
this.state.searchTerm = ”;
this.config.currentPage = 1;
this.render();
}
handleSort(field) {
if (this.state.sortField === field) {
this.state.sortDirection = this.state.sortDirection === ‘asc’ ? ‘desc’ : ‘asc’;
} else {
this.state.sortField = field;
this.state.sortDirection = ‘asc’;
}
this.render();
}
// 数据处理
processData() {
let data = […this.config.data];
// 搜索过滤
if (this.state.searchTerm) {
data = data.filter(item => {
return Object.values(item).some(value =>
value.toString().toLowerCase().includes(this.state.searchTerm)
);
});
}
// 排序
if (this.state.sortField) {
data.sort((a, b) => {
let valA = a[this.state.sortField];
let valB = b[this.state.sortField];
if (typeof valA === ‘number’ && typeof valB === ‘number’) {
return this.state.sortDirection === ‘asc’ ? valA – valB : valB – valA;
}
valA = valA.toString().toLowerCase();
valB = valB.toString().toLowerCase();
if (valA < valB) return this.state.sortDirection === 'asc' ? -1 : 1;
if (valA > valB) return this.state.sortDirection === ‘asc’ ? 1 : -1;
return 0;
});
}
this.state.filteredData = data;
return data;
}
// 获取当前页数据
getCurrentPageData() {
const data = this.processData();
const start = (this.config.currentPage – 1) * this.config.pageSize;
const end = start + this.config.pageSize;
return data.slice(start, end);
}
// 获取总页数
getTotalPages() {
const data = this.processData();
return Math.ceil(data.length / this.config.pageSize);
}
// 渲染数据
renderData() {
const data = this.getCurrentPageData();
if (data.length === 0) {
this.dom.dataContainer.innerHTML = ‘
‘;
return;
}
let html = ”;
data.forEach(item => {
html += `
`;
});
this.dom.dataContainer.innerHTML = html;
}
// 渲染分页信息
renderInfo() {
const data = this.processData();
const totalPages = this.getTotalPages();
const start = (this.config.currentPage – 1) * this.config.pageSize + 1;
const end = Math.min(this.config.currentPage * this.config.pageSize, data.length);
let info = `显示 ${start}-${end} 条,共 ${data.length} 条数据`;
if (totalPages > 1) {
info += `,第 ${this.config.currentPage}/${totalPages} 页`;
}
// 显示搜索和排序状态
if (this.state.searchTerm) {
info += ` | 搜索:”${this.state.searchTerm}”`;
}
if (this.state.sortField) {
const direction = this.state.sortDirection === ‘asc’ ? ‘↑’ : ‘↓’;
info += ` | 排序:${this.state.sortField} ${direction}`;
}
this.dom.infoContainer.textContent = info;
}
// 渲染分页按钮
renderButtons() {
const totalPages = this.getTotalPages();
if (totalPages <= 1) { this.dom.buttonsContainer.innerHTML = ''; return; } let html = ''; // 上一页 const prevDisabled = this.config.currentPage === 1 ? 'disabled' : ''; html += ``;
// 页码按钮
const maxButtons = this.config.maxVisiblePages;
let startPage = Math.max(1, this.config.currentPage – Math.floor(maxButtons / 2));
let endPage = Math.min(totalPages, startPage + maxButtons – 1);
if (endPage – startPage + 1 < maxButtons) {
startPage = Math.max(1, endPage - maxButtons + 1);
}
if (startPage > 1) {
html += ``;
if (startPage > 2) {
html += `…`;
}
}
for (let i = startPage; i <= endPage; i++) {
const activeClass = i === this.config.currentPage ? 'active' : '';
html += ``;
}
if (endPage < totalPages) {
if (endPage < totalPages - 1) {
html += `…`;
}
html += ``;
}
// 下一页
const nextDisabled = this.config.currentPage === totalPages ? ‘disabled’ : ”;
html += ``;
this.dom.buttonsContainer.innerHTML = html;
}
// 分页操作
goToPage(page) {
const totalPages = this.getTotalPages();
if (page < 1 || page > totalPages) return;
this.config.currentPage = page;
this.render();
}
prevPage() {
if (this.config.currentPage > 1) {
this.goToPage(this.config.currentPage – 1);
}
}
nextPage() {
const totalPages = this.getTotalPages();
if (this.config.currentPage < totalPages) {
this.goToPage(this.config.currentPage + 1);
}
}
// 更新数据
updateData(newData) {
this.config.data = newData;
this.config.currentPage = 1;
this.render();
}
// 渲染所有内容
render() {
this.renderData();
this.renderInfo();
this.renderButtons();
}
}
// 使用示例
document.addEventListener('DOMContentLoaded', function() {
// 生成测试数据
function generateData(count = 100) {
const data = [];
for (let i = 1; i <= count; i++) {
data.push({
id: i,
name: `项目 ${i}`,
description: `这是第 ${i} 个项目的详细描述,包含更多信息`,
date: new Date(2023, 0, i).toLocaleDateString('zh-CN'),
status: i % 3 === 0 ? '已完成' : (i % 3 === 1 ? '进行中' : '待处理')
});
}
return data;
}
// 创建分页实例
const pagination = new CompletePagination({
data: generateData(100),
pageSize: 10,
containerId: 'pagination-container',
showSearch: true,
showSort: true,
showPageSizeSelector: true,
maxVisiblePages: 5
});
// 暴露到全局方便调试
window.pagination = pagination;
});
5.2 最佳实践总结
性能优化
:
使用事件委托减少事件监听器
使用DocumentFragment减少DOM操作
对于大数据量考虑虚拟滚动
可维护性
:
使用类封装分页逻辑
配置与逻辑分离
提供清晰的API
用户体验
:
提供搜索和排序功能
响应式设计适配不同设备
清晰的页面信息展示
错误处理
:
验证输入参数
处理边界情况
提供友好的错误提示
六、总结
本文从零开始详细讲解了JavaScript实现前端分页的完整过程,包括:
基础实现
:核心分页逻辑和HTML结构
进阶功能
:搜索、排序、虚拟滚动等
常见问题
:性能、内存泄漏、响应式设计等
完整示例
:一个功能完整的分页组件
通过这些内容,你可以根据实际项目需求选择合适的实现方式。对于简单项目,基础分页类就足够了;对于复杂项目,可以考虑使用完整的分页组件或结合框架(如Vue、React)实现。
记住,好的分页组件应该具备:
清晰的逻辑、良好的性能、优秀的用户体验
。在实际开发中,根据数据量、用户需求和项目复杂度来选择合适的实现方案。