/**
 * 文章归档模块
 */
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初始化状态标记
各 ElementDOM 元素缓存

核心方法解析

init() - 初始化方法

public init(): void
  1. 查找 .archive 容器元素
  2. 从容器的 dataset 属性读取初始配置:
    • pageIndexpageSizetotalCounthasMore
  3. 缓存关键 DOM 元素(容器、加载提示、结束提示)
  4. 设置滚动监听器
  5. 根据 _hasMore 控制加载提示显示

setupScrollListener() - 滚动监听

public setupScrollListener(): void

关键技术点:

  • 使用 requestAnimationFrame 节流优化性能
  • 通过 ticking 标志防止重复触发
  • 计算距离底部的距离(threshold = 500px)
  • 当距离小于阈值且满足加载条件时触发 loadMore()
  • 使用 { passive: true } 优化滚动性能

loadMore() - 异步加载更多

public async loadMore(): Promise<void>

加载流程:

  1. 防重复检查:已在加载中或没有更多数据则返回
  2. 设置加载状态:显示 loading 元素
  3. 发起请求
    fetch(`/Article/ArchiveLoadMore?pageIndex=${nextPage}&pageSize=${pageSize}`)
    
  4. 处理响应
    • 解析 HTML 字符串
    • 提取 .archive__year 元素
    • 追加到容器中
  5. 更新状态
    • 更新页码
    • 检查是否加载完所有数据
  6. 异常处理:捕获并打印错误
  7. 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
    ↓
显示"已全部加载"提示

设计亮点

  1. 单例模式:避免多实例冲突
  2. requestAnimationFrame 节流:优化滚动性能
  3. 状态管理完善:防止重复加载
  4. 错误处理健全:网络异常不影响后续操作
  5. 内存管理:提供 destroy 方法清理资源
  6. 渐进式加载:阈值触发而非到底才加载,提升体验

潜在改进点

  • 可添加重试机制
  • 可支持自定义阈值
  • 可添加加载失败提示 UI
  • 可使用 IntersectionObserver API 替代滚动监听(更现代)
评论加载中...