mirror of
https://github.com/jeffvli/feishin.git
synced 2026-06-10 14:22:46 +02:00
rewrite Image component
- remove react-image dependency - use manual blob load - abort load when exiting viewport
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { ImageRequest } from '/@/shared/types/domain-types';
|
||||
|
||||
type FetchPriority = 'auto' | 'high' | 'low';
|
||||
|
||||
interface NativeImageState {
|
||||
displaySrc?: string;
|
||||
status: 'error' | 'idle' | 'loaded' | 'loading';
|
||||
}
|
||||
|
||||
interface UseNativeImageArgs {
|
||||
enabled: boolean;
|
||||
fetchPriority?: FetchPriority;
|
||||
onFetchError?: () => void;
|
||||
request?: ImageRequest | null;
|
||||
}
|
||||
|
||||
export function useNativeImage({
|
||||
enabled,
|
||||
fetchPriority,
|
||||
onFetchError,
|
||||
request,
|
||||
}: UseNativeImageArgs) {
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
const loadedRequestSignatureRef = useRef<null | string>(null);
|
||||
const objectUrlRef = useRef<null | string>(null);
|
||||
const onFetchErrorRef = useRef(onFetchError);
|
||||
const [state, setState] = useState<NativeImageState>({ status: 'idle' });
|
||||
|
||||
const requestSignature = useMemo(() => {
|
||||
if (!request) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return JSON.stringify({
|
||||
cacheKey: request.cacheKey,
|
||||
credentials: request.credentials,
|
||||
headers: request.headers,
|
||||
url: request.url,
|
||||
});
|
||||
}, [request]);
|
||||
|
||||
onFetchErrorRef.current = onFetchError;
|
||||
|
||||
useEffect(() => {
|
||||
const abortCurrentRequest = () => {
|
||||
abortControllerRef.current?.abort();
|
||||
abortControllerRef.current = null;
|
||||
};
|
||||
|
||||
const revokeObjectUrl = () => {
|
||||
if (!objectUrlRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
URL.revokeObjectURL(objectUrlRef.current);
|
||||
objectUrlRef.current = null;
|
||||
loadedRequestSignatureRef.current = null;
|
||||
};
|
||||
|
||||
if (!request || !requestSignature) {
|
||||
abortCurrentRequest();
|
||||
revokeObjectUrl();
|
||||
setState({ status: 'idle' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!enabled) {
|
||||
abortCurrentRequest();
|
||||
setState((currentState) =>
|
||||
currentState.displaySrc
|
||||
? { ...currentState, status: 'loaded' }
|
||||
: { status: 'idle' },
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (loadedRequestSignatureRef.current === requestSignature && objectUrlRef.current) {
|
||||
setState({ displaySrc: objectUrlRef.current, status: 'loaded' });
|
||||
return;
|
||||
}
|
||||
|
||||
abortCurrentRequest();
|
||||
revokeObjectUrl();
|
||||
setState({ status: 'loading' });
|
||||
|
||||
const abortController = new AbortController();
|
||||
abortControllerRef.current = abortController;
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
const init = {
|
||||
credentials: request.credentials,
|
||||
headers: request.headers,
|
||||
signal: abortController.signal,
|
||||
} as RequestInit & { priority?: FetchPriority };
|
||||
|
||||
if (fetchPriority) {
|
||||
init.priority = fetchPriority;
|
||||
}
|
||||
|
||||
const response = await fetch(request.url, init);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load image: ${response.status}`);
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
|
||||
if (abortController.signal.aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const objectUrl = URL.createObjectURL(blob);
|
||||
objectUrlRef.current = objectUrl;
|
||||
loadedRequestSignatureRef.current = requestSignature;
|
||||
setState({ displaySrc: objectUrl, status: 'loaded' });
|
||||
} catch {
|
||||
if (abortController.signal.aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
revokeObjectUrl();
|
||||
setState({ status: 'error' });
|
||||
onFetchErrorRef.current?.();
|
||||
} finally {
|
||||
if (abortControllerRef.current === abortController) {
|
||||
abortControllerRef.current = null;
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
abortController.abort();
|
||||
|
||||
if (abortControllerRef.current === abortController) {
|
||||
abortControllerRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [enabled, fetchPriority, request, requestSignature]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
abortControllerRef.current?.abort();
|
||||
|
||||
if (objectUrlRef.current) {
|
||||
URL.revokeObjectURL(objectUrlRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
displaySrc: state.displaySrc,
|
||||
isError: state.status === 'error',
|
||||
isLoaded: state.status === 'loaded',
|
||||
isLoading: state.status === 'loading',
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user