refactor(ProcessPostUseCase, useCursorPagination, useDifferentialPosts): enhance post merging and loading states
All checks were successful
Frontend CI / build-and-push-web (push) Successful in 4m57s
Frontend CI / ota-android (push) Successful in 10m48s
Frontend CI / build-android-apk (push) Successful in 52m58s

- Introduced new methods for merging posts and comments efficiently, improving performance during data updates.
- Updated loading state management in hooks to include initial loading and loading more states for better user feedback.
- Refactored HomeScreen and PostDetailScreen to utilize the new loading states and merging logic, enhancing user experience during data fetching.
- Improved pagination handling in useCursorPagination and useDifferentialPosts to ensure consistent data management across components.
This commit is contained in:
lafay
2026-03-24 01:19:09 +08:00
parent aaf53d1c3b
commit 5c9cb6acea
11 changed files with 475 additions and 101 deletions

View File

@@ -109,6 +109,8 @@ class ProcessPostUseCase {
// 当前用户ID
private currentUserId: string | null = null;
private readonly mergePerfWarnThresholdMs = 12;
// 私有构造函数
private constructor() {
this.paginationManager = new PaginationStateManager();
@@ -241,6 +243,83 @@ class ProcessPostUseCase {
this.notifyStateChanged(key);
}
private getEntityId(item: unknown): string | null {
if (!item || typeof item !== 'object') {
return null;
}
const maybeId = (item as { id?: unknown }).id;
if (typeof maybeId === 'string' || typeof maybeId === 'number') {
return String(maybeId);
}
return null;
}
private mergePostListWithFastPath(previousInput: Post[] | null | undefined, incomingInput: Post[] | null | undefined): Post[] {
const previous = Array.isArray(previousInput) ? previousInput : [];
const incoming = Array.isArray(incomingInput) ? incomingInput : [];
if (incoming.length === 0) {
return previous;
}
if (previous.length === 0) {
return incoming;
}
const lastPrevId = this.getEntityId(previous[previous.length - 1]);
const firstIncomingId = this.getEntityId(incoming[0]);
if (lastPrevId && firstIncomingId && lastPrevId !== firstIncomingId) {
const prevIdSet = new Set(previous.map((item) => this.getEntityId(item)).filter(Boolean) as string[]);
if (!prevIdSet.has(firstIncomingId)) {
return [...previous, ...incoming];
}
}
const merged = [...previous];
const indexById = new Map<string, number>();
for (let i = 0; i < merged.length; i += 1) {
const id = this.getEntityId(merged[i]);
if (id) {
indexById.set(id, i);
}
}
for (const post of incoming) {
const id = this.getEntityId(post);
if (!id) {
merged.push(post);
continue;
}
const existingIndex = indexById.get(id);
if (existingIndex === undefined) {
indexById.set(id, merged.length);
merged.push(post);
} else {
merged[existingIndex] = post;
}
}
return merged;
}
private mergeRefreshWindow(previousInput: Post[] | null | undefined, latestInput: Post[] | null | undefined): Post[] {
const previous = Array.isArray(previousInput) ? previousInput : [];
const latest = Array.isArray(latestInput) ? latestInput : [];
if (latest.length === 0) {
return previous;
}
if (previous.length === 0) {
return latest;
}
const latestIdSet = new Set(
latest.map((item) => this.getEntityId(item)).filter(Boolean) as string[]
);
const remaining = previous.filter((item) => {
const id = this.getEntityId(item);
return !id || !latestIdSet.has(id);
});
return [...latest, ...remaining];
}
/**
* 更新帖子详情状态
* @param postId 帖子ID
@@ -280,8 +359,17 @@ class ProcessPostUseCase {
const isCursorMode = result.nextCursor !== undefined && result.nextCursor !== null;
// 更新状态(保存请求参数以便加载更多时使用)
const mergeStart = Date.now();
const currentState = this.getPostsState(key);
const incomingPosts = Array.isArray(result.posts) ? result.posts : [];
const mergedPosts = this.mergeRefreshWindow(currentState.posts, incomingPosts);
const mergeCost = Date.now() - mergeStart;
if (mergeCost >= this.mergePerfWarnThresholdMs) {
console.log('[PostPerf] fetchPosts merge cost', { key, costMs: mergeCost, prev: currentState.posts.length, next: incomingPosts.length });
}
this.updatePostsState(key, {
posts: result.posts,
posts: mergedPosts,
hasMore: result.hasMore,
cursor: isCursorMode ? result.nextCursor : null, // cursor 模式保存 cursor否则设为 null 表示 page 模式
currentPage: isCursorMode ? undefined : 1, // page 模式从第 1 页开始
@@ -293,7 +381,7 @@ class ProcessPostUseCase {
// 通知订阅者
this.notifySubscribers({
type: 'posts_loaded',
payload: { key, posts: result.posts, hasMore: result.hasMore },
payload: { key, posts: incomingPosts, hasMore: result.hasMore },
timestamp: Date.now(),
});
@@ -338,8 +426,16 @@ class ProcessPostUseCase {
const isCursorMode = result.nextCursor !== undefined && result.nextCursor !== null;
// 更新状态
const mergeStart = Date.now();
const incomingPosts = Array.isArray(result.posts) ? result.posts : [];
const mergedPosts = this.mergeRefreshWindow(state.posts, incomingPosts);
const mergeCost = Date.now() - mergeStart;
if (mergeCost >= this.mergePerfWarnThresholdMs) {
console.log('[PostPerf] refreshPosts merge cost', { key, costMs: mergeCost, prev: state.posts.length, next: incomingPosts.length });
}
this.updatePostsState(key, {
posts: result.posts,
posts: mergedPosts,
hasMore: result.hasMore,
cursor: isCursorMode ? result.nextCursor : null,
currentPage: isCursorMode ? undefined : 1,
@@ -417,7 +513,13 @@ class ProcessPostUseCase {
const result = await postRepository.getPosts(queryParams);
// 合并新数据
const newPosts = [...state.posts, ...result.posts];
const mergeStart = Date.now();
const incomingPosts = Array.isArray(result.posts) ? result.posts : [];
const newPosts = this.mergePostListWithFastPath(state.posts, incomingPosts);
const mergeCost = Date.now() - mergeStart;
if (mergeCost >= this.mergePerfWarnThresholdMs) {
console.log('[PostPerf] loadMore merge cost', { key, costMs: mergeCost, prev: state.posts.length, incoming: incomingPosts.length, merged: newPosts.length });
}
// 判断响应是否为 cursor 模式(有 next_cursor 返回)
const responseIsCursorMode = result.nextCursor !== undefined && result.nextCursor !== null;
@@ -433,7 +535,7 @@ class ProcessPostUseCase {
});
return {
posts: result.posts,
posts: incomingPosts,
hasMore: result.hasMore,
nextCursor: result.nextCursor,
};