/**
* 文章归档模块
*/
export class Archive {
private static _instance: Archive | null = null;
private _loading: boolean = false;
private _hasMore: boolean = true;
private _pageIndex: number = 1;
private _pageSize: number = 50;
private _totalCount: number = 0;
private _scrollHandler: ((this: Window, ev: Event) => void) | null = null;
private _isInitialized: boolean = false;
private _archiveContainer: HTMLElement | null = null;
private _loadingElement: HTMLElement | null = null;
private _endElement: HTMLElement | null = null;
public constructor() {
if (Archive._instance) {
Archive._instance.destroy();
}
this._loading = false;
this._hasMore = true;
this._pageIndex = 1;
this._pageSize = 50;
this._totalCount = 0;
this._scrollHandler = null;
this._isInitialized = false;
Archive._instance = this;
this.init();
}
public init(): void {
const archiveContainer = document.querySelector<HTMLElement>('.archive');
if (!archiveContainer) {
return;
}
this._pageIndex = parseInt(archiveContainer.dataset.pageIndex as string) || 1;
this._pageSize = parseInt(archiveContainer.dataset.pageSize as string) || 50;
this._totalCount = parseInt(archiveContainer.dataset.totalCount as string) || 0;
this._hasMore = archiveContainer.dataset.hasMore === 'true';
this._archiveContainer = archiveContainer;
this._loadingElement = document.querySelector<HTMLElement>('.archive__loading');
this._endElement = document.querySelector<HTMLElement>('.archive__end');
this.setupScrollListener();
if (this._loadingElement) {
this._loadingElement.style.display = this._hasMore ? 'flex' : 'none';
}
this._isInitialized = true;
}
public destroy(): void {
if (this._scrollHandler) {
window.removeEventListener('scroll', this._scrollHandler);
this._scrollHandler = null;
}
this._isInitialized = false;
if (Archive._instance === this) {
Archive._instance = null;
}
}
public setupScrollListener(): void {
let ticking = false;
const checkScroll = (): void => {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const windowHeight = window.innerHeight;
const documentHeight = document.documentElement.scrollHeight;
const threshold = 500;
const distanceToBottom = documentHeight - (scrollTop + windowHeight);
if (distanceToBottom < threshold && this._hasMore && !this._loading) {
void this.loadMore();
}
ticking = false;
};
this._scrollHandler = () => {
if (!ticking) {
window.requestAnimationFrame(checkScroll);
ticking = true;
}
};
window.addEventListener('scroll', this._scrollHandler, { passive: true });
}
public async loadMore(): Promise<void> {
if (this._loading || !this._hasMore) {
return;
}
this._loading = true;
if (this._loadingElement) {
this._loadingElement.style.display = 'flex';
}
try {
const nextPage = this._pageIndex + 1;
const url = `/Article/ArchiveLoadMore?pageIndex=${nextPage}&pageSize=${this._pageSize}`;
const response = await fetch(url);
if (!response.ok) {
throw new Error('Network response was not ok');
}
const html = await response.text();
if (html.trim() === '') {
this._hasMore = false;
this.showEndMessage();
return;
}
const tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
const newYears = tempDiv.querySelectorAll<HTMLElement>('.archive__year');
if (newYears.length === 0) {
this._hasMore = false;
this.showEndMessage();
return;
}
newYears.forEach((year) => {
if (this._archiveContainer) {
this._archiveContainer.appendChild(year);
}
});
this._pageIndex = nextPage;
const loadedSoFar = this._pageIndex * this._pageSize;
if (loadedSoFar >= this._totalCount) {
this._hasMore = false;
this.showEndMessage();
}
} catch (error) {
console.error('加载归档失败:', error);
} finally {
this._loading = false;
if (this._loadingElement) {
this._loadingElement.style.display = 'none';
}
}
}
public showEndMessage(): void {
if (this._loadingElement) {
this._loadingElement.style.display = 'none';
}
if (this._endElement) {
this._endElement.style.display = 'flex';
}
}
}
⚠⚠ 以下内容为AI分析的结果,请根据实际情况进行判断。
代码解释:文章归档模块
这是一个实现文章归档无限滚动加载功能的 TypeScript 类。以下是详细解析:
核心功能
实现当用户滚动到页面底部时,自动加载更多归档文章的功能。
类结构分析
1. 单例模式实现
private static _instance: Archive | null = null;
- 使用静态私有变量存储唯一实例
- 在构造函数中,如果已存在实例则先销毁旧实例
- 保证全局只有一个 Archive 实例运行
2. 私有状态属性
| 属性 | 作用 |
|---|---|
_loading | 防止重复加载的锁 |
_hasMore | 是否还有更多数据 |
_pageIndex | 当前页码 |
_pageSize | 每页数量 |
_totalCount | 总数据量 |
_scrollHandler | 滚动事件处理函数引用 |
_isInitialized | 初始化状态标记 |
| 各 Element | DOM 元素缓存 |
核心方法解析
init() - 初始化方法
public init(): void
- 查找
.archive容器元素 - 从容器的
dataset属性读取初始配置:pageIndex、pageSize、totalCount、hasMore
- 缓存关键 DOM 元素(容器、加载提示、结束提示)
- 设置滚动监听器
- 根据
_hasMore控制加载提示显示
setupScrollListener() - 滚动监听
public setupScrollListener(): void
关键技术点:
- 使用
requestAnimationFrame节流优化性能 - 通过
ticking标志防止重复触发 - 计算距离底部的距离(threshold = 500px)
- 当距离小于阈值且满足加载条件时触发
loadMore() - 使用
{ passive: true }优化滚动性能
loadMore() - 异步加载更多
public async loadMore(): Promise<void>
加载流程:
- 防重复检查:已在加载中或没有更多数据则返回
- 设置加载状态:显示 loading 元素
- 发起请求:
fetch(`/Article/ArchiveLoadMore?pageIndex=${nextPage}&pageSize=${pageSize}`) - 处理响应:
- 解析 HTML 字符串
- 提取
.archive__year元素 - 追加到容器中
- 更新状态:
- 更新页码
- 检查是否加载完所有数据
- 异常处理:捕获并打印错误
- finally 块:重置加载状态,隐藏 loading
destroy() - 销毁方法
public destroy(): void
- 移除滚动事件监听器
- 重置初始化标记
- 清空单例引用
- 防止内存泄漏
showEndMessage() - 显示结束提示
public showEndMessage(): void
- 隐藏加载提示
- 显示"已加载全部"提示
工作流程示例
用户打开归档页面
↓
构造函数执行 → 销毁旧实例 → 初始化
↓
读取初始数据(pageIndex=1, totalCount=200)
↓
设置滚动监听(距底部<500px触发)
↓
用户滚动到底部
↓
loadMore() → fetch第2页数据
↓
追加新的 .archive__year 元素
↓
更新 pageIndex=2
↓
继续监听滚动...
↓
当 pageIndex * pageSize >= totalCount
↓
显示"已全部加载"提示
设计亮点
- 单例模式:避免多实例冲突
- requestAnimationFrame 节流:优化滚动性能
- 状态管理完善:防止重复加载
- 错误处理健全:网络异常不影响后续操作
- 内存管理:提供 destroy 方法清理资源
- 渐进式加载:阈值触发而非到底才加载,提升体验
潜在改进点
- 可添加重试机制
- 可支持自定义阈值
- 可添加加载失败提示 UI
- 可使用 IntersectionObserver API 替代滚动监听(更现代)
AI 正在分析代码…
评论加载中...