export class Timeline {
static _instance = null;
constructor() {
// 如果已存在实例,先销毁旧实例
if (Timeline._instance) {
Timeline._instance.destroy();
}
this._loading = false;
this._hasMore = true;
this._pageIndex = 1;
this._pageSize = 10;
this._account = 'pengqian';
this._scrollHandler = null;
this._isInitialized = false;
Timeline._instance = this;
this.init();
}
init() {
// 检测是否存在时间轴结构
const timelineContainer = document.querySelector('.timeline');
if (!timelineContainer) {
// 没有时间轴结构,不进行任何初始化
return;
}
// 从容器读取初始配置
this._pageIndex = parseInt(timelineContainer.dataset.pageIndex) || 1;
this._pageSize = parseInt(timelineContainer.dataset.pageSize) || 10;
this._account = timelineContainer.dataset.account || 'pengqian';
this._hasMore = timelineContainer.dataset.hasMore === 'true';
this._timelineContainer = timelineContainer;
this._loadingElement = document.querySelector('.timeline__loading');
this._endElement = document.querySelector('.timeline__end');
// 初始化已有项目的动画
this.initItemsAnimation();
// 设置滚动监听
this.setupScrollListener();
// 初始化时隐藏加载提示
if (this._loadingElement) {
this._loadingElement.style.display = this._hasMore ? 'flex' : 'none';
}
this._isInitialized = true;
}
destroy() {
// 清理滚动事件监听器
if (this._scrollHandler) {
window.removeEventListener('scroll', this._scrollHandler);
this._scrollHandler = null;
}
this._isInitialized = false;
// 清理实例引用
if (Timeline._instance === this) {
Timeline._instance = null;
}
}
initItemsAnimation() {
const timelineItems = document.querySelectorAll('.timeline__item');
if (timelineItems.length === 0) return;
const observerOptions = {
root: null,
rootMargin: '0px',
threshold: 0.1
};
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('is-visible');
observer.unobserve(entry.target);
}
});
}, observerOptions);
timelineItems.forEach(item => {
observer.observe(item);
});
}
setupScrollListener() {
let ticking = false;
const checkScroll = () => {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const windowHeight = window.innerHeight;
const documentHeight = document.documentElement.scrollHeight;
// 距离底部 500px 时开始加载
const threshold = 500;
const distanceToBottom = documentHeight - (scrollTop + windowHeight);
if (distanceToBottom < threshold && this._hasMore && !this._loading) {
this.loadMore();
}
ticking = false;
};
this._scrollHandler = () => {
if (!ticking) {
window.requestAnimationFrame(checkScroll);
ticking = true;
}
};
window.addEventListener('scroll', this._scrollHandler, { passive: true });
}
async loadMore() {
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 = `/Timeline/LoadMore?account=${encodeURIComponent(this._account)}&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 newItems = tempDiv.querySelectorAll('.timeline__item');
if (newItems.length === 0) {
this._hasMore = false;
this.showEndMessage();
return;
}
// 追加新项目
newItems.forEach(item => {
this._timelineContainer.appendChild(item);
});
// 为新项目添加动画
this.initItemsAnimation();
// 更新页码
this._pageIndex = nextPage;
// 检查是否还有更多
// 如果返回的项目数量少于 pageSize,说明没有更多了
if (newItems.length < this._pageSize) {
this._hasMore = false;
this.showEndMessage();
}
} catch (error) {
console.error('加载时间轴失败:', error);
} finally {
this._loading = false;
if (this._loadingElement) {
this._loadingElement.style.display = 'none';
}
}
}
showEndMessage() {
if (this._loadingElement) {
this._loadingElement.style.display = 'none';
}
if (this._endElement) {
this._endElement.style.display = 'flex';
}
}
}
⚠⚠ 以下内容为AI分析的结果,请根据实际情况进行判断。
下面按要点解释这段 Timeline 类的功能、工作流程和各方法的作用,同时列出几个值得注意的细节/改进建议。
总体功能概述
- 这是一个前端的“时间轴/分页加载”组件类(Timeline),实现了基于滚动的自动加载更多条目(类似无限滚动)以及进入可视区时的逐项动画(通过 IntersectionObserver)。
- 使用了简单的单例机制(静态属性 Timeline._instance)确保页面上只有一个实例在起作用;若构造新实例,会先销毁已有实例。
- 从 DOM(.timeline 容器的 data-* 属性)读取初始配置(pageIndex、pageSize、account、hasMore),通过 fetch 向 /Timeline/LoadMore 请求下一页 HTML,然后把返回的 .timeline__item 加到容器里,并为它们触发入场动画。
逐方法说明
- constructor()
- 如果已有实例(Timeline._instance),会调用其 destroy() 先销毁旧实例。
- 初始化内部状态变量:_loading、_hasMore、_pageIndex、_pageSize、_account 等。
- 保存实例到 Timeline._instance 并立即调用 this.init() 进行初始化。
- init()
- 查找页面上第一个 .timeline 容器;如果找不到则直接返回(不做初始化)。
- 从容器的 data-* 属性读取 pageIndex、pageSize、account、hasMore,用于后续分页请求与状态。
- 缓存常用 DOM 节点:_timelineContainer、_loadingElement(.timeline__loading)、_endElement(.timeline__end)。
- 调用 initItemsAnimation() 为当前已有的项目安装可视区入场动画(IntersectionObserver)。
- 调用 setupScrollListener() 在 window 上注册滚动监听,用于触发加载更多。
- 根据 hasMore 决定是否显示加载提示(loading element)。
- 标记 _isInitialized = true。
- destroy()
- 移除 window 的滚动事件监听(如果存在),把 _scrollHandler 清空。
- 置 _isInitialized = false。
- 如果当前实例是静态单例,清除 Timeline._instance 的引用。
- initItemsAnimation()
- 查询所有 .timeline__item 元素(全局查询,而不是限定到容器)。
- 使用 IntersectionObserver(threshold 0.1)观察这些元素:一旦元素进入可视区(isIntersecting),就给元素添加 is-visible 类,并对该元素调用 observer.unobserve,确保每个元素只触发一次动画。
- 每次调用都会创建新的 IntersectionObserver 并为当前项注册观察。
- setupScrollListener()
- 实现了基于 requestAnimationFrame 的简单节流(ticking 标志)。
- 每次滚动时,使用 requestAnimationFrame 调用 checkScroll(),避免高频滚动直接触发大量计算。
- checkScroll() 计算页面距离底部的距离 distanceToBottom;当 distanceToBottom < 500(阈值为 500px)、且有更多(_hasMore)且当前不在加载(!_loading)时,调用 loadMore()。
- loadMore()
- 防止并发调用:若 _loading 为 true 或 _hasMore 为 false,则直接返回。
- 设置 _loading = true,并显示 _loadingElement(如果有)。
- 构造下一页 URL:/Timeline/LoadMore?account=...&pageIndex=nextPage&pageSize=...
- 用 fetch 请求该 URL,读取文本(html)。
- 若返回空字符串或解析后无 .timeline__item,则把 _hasMore 设为 false 并调用 showEndMessage()。
- 若返回含有新项,则把新项追加到 _timelineContainer 中,并为新项调用 initItemsAnimation()(注册动画观察)。
- 更新 _pageIndex 到 nextPage;如果新项数量 < pageSize,则推断没有更多,设置 _hasMore = false 并 showEndMessage()。
- 无论成功或失败,finally 中把 _loading 设为 false 并隐藏 loading 元素。
- showEndMessage()
- 隐藏加载提示元素(_loadingElement),显示结束提示元素(_endElement,.timeline__end),表示已到末尾没有更多内容。
值得注意的问题与改进建议(简要)
- initItemsAnimation 每次都会创建新的 IntersectionObserver,但并不保存/断开前一个 observer,这可能导致未必要的多次 observer 实例(可考虑把 observer 存为实例属性并在 destroy 中 disconnect)。
- initItemsAnimation、querySelectorAll 等操作是全局查询(document)而不是限定在 this._timelineContainer 内,若页面上有多个类似结构或其它区域也用同类类名,可能会互相影响。建议把选择器基于 this._timelineContainer 限定范围。
- 构造函数中直接销毁旧实例这一策略简单,但可能在某些场景下引发状态竞争或页面闪烁,取决于何时创建实例。通常建议在应用初始化阶段只创建一次实例,或使用工厂函数控制生命周期。
- 使用 innerHTML 将返回的 HTML 注入页面,需确保后端返回的内容可信,以避免 XSS 风险。
- loadMore 在 fetch 请求失败时只 console.error,用户体验上可能需要显示错误提示或重试机制。
- pageIndex 的更新策略是请求成功后才 ++,这是合理的,但要注意并发请求场景(当前逻辑已用 _loading 阻止并发)。
- 锚点/滚动阈值(500px)是硬编码,可能需要根据设计调整或做成可配置项。
- 如果页面中动态移除或替换 timeline 容器,destroy() 需要配合调用以移除监听器并释放资源。
总结
- 这个类实现了一个单例式的时间轴组件:初始化时读取 DOM 配置,给已有条目添加“进入可视区动画”,在滚动接近底部时自动 fetch 下一页 HTML 并追加到时间轴,管理加载状态与“没有更多”的展示。代码结构清晰,核心点为 IntersectionObserver(动画) + requestAnimationFrame 节流的滚动监听 + fetch 加载更多。
评论加载中...