fix: 修复前端快速滑动时多个网络请求

This commit is contained in:
feie9454 2026-06-09 17:32:35 +08:00
parent 681e3c7493
commit ca654754ae

View File

@ -14,20 +14,22 @@ import { useStarToggle } from '../hooks/useStarToggle';
const inFlightJsonRequests = new Map<string, Promise<unknown>>();
const fetchJson = async <T,>(url: string, signal?: AbortSignal): Promise<T> => {
const response = await fetch(url, { cache: 'no-store', signal });
if (!response.ok) {
throw new Error(`Request failed with status ${response.status}`);
}
return response.json() as Promise<T>;
};
const fetchJsonOnce = async <T,>(url: string): Promise<T> => {
const existingRequest = inFlightJsonRequests.get(url);
if (existingRequest) {
return existingRequest as Promise<T>;
}
const request = fetch(url, { cache: 'no-store' })
.then(async (response) => {
if (!response.ok) {
throw new Error(`Request failed with status ${response.status}`);
}
return response.json() as Promise<T>;
})
const request = fetchJson<T>(url)
.finally(() => {
inFlightJsonRequests.delete(url);
});
@ -36,6 +38,12 @@ const fetchJsonOnce = async <T,>(url: string): Promise<T> => {
return request as Promise<T>;
};
const isAbortError = (error: unknown) => (
error instanceof DOMException && error.name === 'AbortError'
);
const HOURLY_RECORDS_REQUEST_DEBOUNCE_MS = 120;
interface ScreenshotsTabProps {
hostname: string;
selectedDate: string | null;
@ -53,6 +61,8 @@ export default function ScreenshotsTab({
}: ScreenshotsTabProps) {
const timeDistributionRequestIdRef = useRef(0);
const hourlyRecordsRequestIdRef = useRef(0);
const hourlyRecordsAbortControllerRef = useRef<AbortController | null>(null);
const hourlyRecordsDebounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
// 状态管理
const [timeDistribution, setTimeDistribution] = useState<TimeDistributionPoint[]>([]);
@ -83,6 +93,25 @@ export default function ScreenshotsTab({
const { updatingStars, toggleStar } = useStarToggle();
const clearPendingHourlyRecordsRequest = () => {
if (hourlyRecordsDebounceTimerRef.current) {
clearTimeout(hourlyRecordsDebounceTimerRef.current);
hourlyRecordsDebounceTimerRef.current = null;
}
};
const scheduleHourlyRecordsFetch = (
startTime: number,
endTime: number,
options?: { targetRecordId?: string, keepSelection?: boolean }
) => {
clearPendingHourlyRecordsRequest();
hourlyRecordsDebounceTimerRef.current = setTimeout(() => {
hourlyRecordsDebounceTimerRef.current = null;
fetchHourlyRecords(startTime, endTime, options);
}, HOURLY_RECORDS_REQUEST_DEBOUNCE_MS);
};
// 获取时间分布数据
const fetchTimeDistribution = async () => {
const requestId = ++timeDistributionRequestIdRef.current;
@ -112,19 +141,23 @@ export default function ScreenshotsTab({
// 获取小时记录
const fetchHourlyRecords = async (startTime: number, endTime: number, options?: { targetRecordId?: string, keepSelection?: boolean }) => {
clearPendingHourlyRecordsRequest();
const requestId = ++hourlyRecordsRequestIdRef.current;
const requestUrl = `/hosts/${hostname}/screenshots?startTime=${startTime}&endTime=${endTime}`;
hourlyRecordsAbortControllerRef.current?.abort();
const abortController = new AbortController();
hourlyRecordsAbortControllerRef.current = abortController;
try {
setLoadingRecords(true);
setShowDetailTimeline(true);
const data = await fetchJsonOnce<{
const data = await fetchJson<{
lastUpdate: string | null;
records: ScreenRecord[];
}>(requestUrl);
}>(requestUrl, abortController.signal);
if (requestId !== hourlyRecordsRequestIdRef.current) {
if (requestId !== hourlyRecordsRequestIdRef.current || abortController.signal.aborted) {
return;
}
@ -134,6 +167,10 @@ export default function ScreenshotsTab({
setTimeRange({ min: startTime * 1000, max: endTime * 1000 });
requestAnimationFrame(() => {
if (requestId !== hourlyRecordsRequestIdRef.current || abortController.signal.aborted) {
return;
}
if (options?.targetRecordId) {
const found = newRecords.find((r: ScreenRecord) => r.id === options.targetRecordId);
if (found) {
@ -151,11 +188,19 @@ export default function ScreenshotsTab({
}
});
} catch (error) {
if (isAbortError(error)) {
return;
}
if (requestId === hourlyRecordsRequestIdRef.current) {
console.error('获取记录数据失败:', error);
}
} finally {
if (requestId === hourlyRecordsRequestIdRef.current) {
if (hourlyRecordsAbortControllerRef.current === abortController) {
hourlyRecordsAbortControllerRef.current = null;
}
if (requestId === hourlyRecordsRequestIdRef.current && !abortController.signal.aborted) {
setLoadingRecords(false);
}
}
@ -273,12 +318,8 @@ export default function ScreenshotsTab({
// 事件处理函数
const onHourlySliderChange = (newValue: number) => {
const selectedSec = Math.floor(newValue / 3600000) * 3600;
// 使用 setTimeout 避免在渲染过程中更新状态
setTimeout(() => {
setHourlySliderValue(newValue);
fetchHourlyRecords(selectedSec, selectedSec + 3600);
}, 0);
setHourlySliderValue(newValue);
scheduleHourlyRecordsFetch(selectedSec, selectedSec + 3600);
};
const onDetailedSliderChange = (newValue: number) => {
@ -443,6 +484,19 @@ export default function ScreenshotsTab({
}, [prevFrame, nextFrame]);
// Effects
useEffect(() => {
return () => {
if (hourlyRecordsDebounceTimerRef.current) {
clearTimeout(hourlyRecordsDebounceTimerRef.current);
hourlyRecordsDebounceTimerRef.current = null;
}
hourlyRecordsRequestIdRef.current += 1;
hourlyRecordsAbortControllerRef.current?.abort();
hourlyRecordsAbortControllerRef.current = null;
};
}, []);
useEffect(() => {
fetchTimeDistribution();
}, [hostname]);