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 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]);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user