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