增加请求错误处理

This commit is contained in:
lsy 2025-05-03 20:12:52 +08:00
parent d3e0eddff7
commit 1336dc9081
8 changed files with 582 additions and 50 deletions

View File

@ -547,9 +547,23 @@ const ArticleFilter: React.FC<ArticleFilterProps> = ({ searchParams = {} }) => {
// 添加客户端标记变量,确保只在客户端渲染某些组件 // 添加客户端标记变量,确保只在客户端渲染某些组件
const [isClient, setIsClient] = useState(false); const [isClient, setIsClient] = useState(false);
// 添加 AbortController 引用和组件挂载状态引用
const abortControllerRef = useRef<AbortController | null>(null);
const isMountedRef = useRef<boolean>(true);
// 组件挂载时设置客户端标记 // 组件挂载时设置客户端标记
useEffect(() => { useEffect(() => {
setIsClient(true); setIsClient(true);
// 组件卸载时的清理
return () => {
isMountedRef.current = false;
// 取消进行中的请求
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
};
}, []); }, []);
// 处理searchParams确保我们有正确的参数格式 // 处理searchParams确保我们有正确的参数格式
@ -692,6 +706,9 @@ const ArticleFilter: React.FC<ArticleFilterProps> = ({ searchParams = {} }) => {
const filterParamsJson = JSON.stringify(filterParams); const filterParamsJson = JSON.stringify(filterParams);
const result = await wasmModule.ArticleFilterJS.filter_articles(filterParamsJson); const result = await wasmModule.ArticleFilterJS.filter_articles(filterParamsJson);
// 检查组件是否仍然挂载
if (!isMountedRef.current) return;
// 处理结果 // 处理结果
if (!result || typeof result !== 'object') { if (!result || typeof result !== 'object') {
console.error("[筛选] WASM返回结果格式错误"); console.error("[筛选] WASM返回结果格式错误");
@ -721,10 +738,13 @@ const ArticleFilter: React.FC<ArticleFilterProps> = ({ searchParams = {} }) => {
} }
} }
// 检查组件是否仍然挂载
if (!isMountedRef.current) return;
// 检查并修复总数 // 检查并修复总数
const total = typeof result.total === 'number' ? result.total : articles.length; const total = typeof result.total === 'number' ? result.total : articles.length;
const totalPages = typeof result.total_pages === 'number' ? result.total_pages : const totalPages = typeof result.total_pages === 'number' ? result.total_pages :
Math.ceil(total / filters.pageSize); Math.ceil(total / filters.pageSize);
// 更新状态时提供明确的默认值 // 更新状态时提供明确的默认值
setFilteredArticles(articles); setFilteredArticles(articles);
@ -732,10 +752,16 @@ const ArticleFilter: React.FC<ArticleFilterProps> = ({ searchParams = {} }) => {
setTotalPages(totalPages || 1); setTotalPages(totalPages || 1);
} catch (error) { } catch (error) {
// 检查组件是否仍然挂载
if (!isMountedRef.current) return;
console.error("[筛选] 应用筛选逻辑出错:", error); console.error("[筛选] 应用筛选逻辑出错:", error);
setError("筛选文章时出错,请刷新页面重试"); setError("筛选文章时出错,请刷新页面重试");
} finally { } finally {
setIsLoading(false); // 检查组件是否仍然挂载
if (isMountedRef.current) {
setIsLoading(false);
}
} }
}; };
@ -746,11 +772,22 @@ const ArticleFilter: React.FC<ArticleFilterProps> = ({ searchParams = {} }) => {
const wasm = await import( const wasm = await import(
"@/assets/wasm/article-filter/article_filter.js" "@/assets/wasm/article-filter/article_filter.js"
); );
// 检查组件是否仍然挂载
if (!isMountedRef.current) return;
if (typeof wasm.default === "function") { if (typeof wasm.default === "function") {
await wasm.default(); await wasm.default();
} }
// 再次检查组件是否仍然挂载
if (!isMountedRef.current) return;
setWasmModule(wasm as unknown as ArticleFilterWasm); setWasmModule(wasm as unknown as ArticleFilterWasm);
} catch (err) { } catch (err) {
// 检查组件是否仍然挂载
if (!isMountedRef.current) return;
console.error("加载WASM模块失败:", err); console.error("加载WASM模块失败:", err);
setError("加载筛选模块失败,请刷新页面重试"); setError("加载筛选模块失败,请刷新页面重试");
} }
@ -766,9 +803,22 @@ const ArticleFilter: React.FC<ArticleFilterProps> = ({ searchParams = {} }) => {
const loadIndexData = async () => { const loadIndexData = async () => {
// 取消之前的请求
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
// 创建新的 AbortController
abortControllerRef.current = new AbortController();
try { try {
setIsLoading(true); setIsLoading(true);
const response = await fetch("/index/filter_index.bin"); const response = await fetch("/index/filter_index.bin", {
signal: abortControllerRef.current.signal
});
// 检查组件是否仍然挂载
if (!isMountedRef.current) return;
if (!response.ok) { if (!response.ok) {
throw new Error(`获取筛选索引失败: ${response.statusText}`); throw new Error(`获取筛选索引失败: ${response.statusText}`);
@ -776,28 +826,56 @@ const ArticleFilter: React.FC<ArticleFilterProps> = ({ searchParams = {} }) => {
const indexData = await response.arrayBuffer(); const indexData = await response.arrayBuffer();
// 检查组件是否仍然挂载
if (!isMountedRef.current) return;
// 初始化WASM模块 // 初始化WASM模块
try { try {
await wasmModule.ArticleFilterJS.init(new Uint8Array(indexData)); await wasmModule.ArticleFilterJS.init(new Uint8Array(indexData));
// 检查组件是否仍然挂载
if (!isMountedRef.current) return;
// 获取所有标签 // 获取所有标签
const tags = (await wasmModule.ArticleFilterJS.get_all_tags()) || []; const tags = (await wasmModule.ArticleFilterJS.get_all_tags()) || [];
// 检查组件是否仍然挂载
if (!isMountedRef.current) return;
setAllAvailableTags(tags); setAllAvailableTags(tags);
// 初始加载时不依赖applyFilters函数而是直接执行筛选逻辑 // 初始加载时不依赖applyFilters函数而是直接执行筛选逻辑
// 这避免了循环依赖问题 // 这避免了循环依赖问题
await initialLoadArticles(); await initialLoadArticles();
// 检查组件是否仍然挂载
if (!isMountedRef.current) return;
setIsArticlesLoaded(true); setIsArticlesLoaded(true);
} catch (parseError) { } catch (parseError) {
// 检查组件是否仍然挂载
if (!isMountedRef.current) return;
console.error("解析筛选索引数据失败:", parseError); console.error("解析筛选索引数据失败:", parseError);
setError("索引文件存在但格式不正确,需要重新构建索引"); setError("索引文件存在但格式不正确,需要重新构建索引");
} }
} catch (fetchError) { } catch (fetchError) {
// 检查组件是否仍然挂载
if (!isMountedRef.current) return;
// 如果是取消的请求,不显示错误
if (fetchError instanceof Error && (fetchError.name === 'AbortError' || fetchError.message.includes('aborted'))) {
console.log('索引加载请求被取消:', fetchError.message);
return;
}
console.error("获取索引数据失败:", fetchError); console.error("获取索引数据失败:", fetchError);
setError("索引文件缺失或无法读取,请重新构建索引"); setError("索引文件缺失或无法读取,请重新构建索引");
} finally { } finally {
setIsLoading(false); // 检查组件是否仍然挂载
if (isMountedRef.current) {
setIsLoading(false);
}
} }
}; };
@ -830,6 +908,9 @@ const ArticleFilter: React.FC<ArticleFilterProps> = ({ searchParams = {} }) => {
filterParamsJson, filterParamsJson,
); );
// 检查组件是否仍然挂载
if (!isMountedRef.current) return;
// 检查结果格式 // 检查结果格式
if (!result || typeof result !== 'object') { if (!result || typeof result !== 'object') {
console.error("WASM返回结果格式错误"); console.error("WASM返回结果格式错误");
@ -859,6 +940,9 @@ const ArticleFilter: React.FC<ArticleFilterProps> = ({ searchParams = {} }) => {
} }
} }
// 检查组件是否仍然挂载
if (!isMountedRef.current) return;
// 检查并修复总数 // 检查并修复总数
const total = typeof result.total === 'number' ? result.total : articles.length; const total = typeof result.total === 'number' ? result.total : articles.length;
const totalPages = typeof result.total_pages === 'number' ? result.total_pages : const totalPages = typeof result.total_pages === 'number' ? result.total_pages :
@ -870,7 +954,7 @@ const ArticleFilter: React.FC<ArticleFilterProps> = ({ searchParams = {} }) => {
setTotalPages(totalPages || 1); setTotalPages(totalPages || 1);
// 只有在非初始加载时才更新URL参数 // 只有在非初始加载时才更新URL参数
if (!skipUrlUpdate) { if (!skipUrlUpdate && typeof window !== 'undefined') {
// 初始化URL参数 // 初始化URL参数
const params = new URLSearchParams(); const params = new URLSearchParams();
@ -910,10 +994,16 @@ const ArticleFilter: React.FC<ArticleFilterProps> = ({ searchParams = {} }) => {
return result; return result;
} catch (wasmError) { } catch (wasmError) {
// 检查组件是否仍然挂载
if (!isMountedRef.current) return;
console.error("WASM执行失败"); console.error("WASM执行失败");
throw new Error(`WASM执行失败`); throw new Error(`WASM执行失败`);
} }
} catch (error) { } catch (error) {
// 检查组件是否仍然挂载
if (!isMountedRef.current) return;
console.error("初始加载文章出错"); console.error("初始加载文章出错");
setError("加载文章时出错,请刷新页面重试"); setError("加载文章时出错,请刷新页面重试");
return { return {
@ -924,11 +1014,21 @@ const ArticleFilter: React.FC<ArticleFilterProps> = ({ searchParams = {} }) => {
total_pages: 0, total_pages: 0,
}; };
} finally { } finally {
setIsLoading(false); // 检查组件是否仍然挂载
if (isMountedRef.current) {
setIsLoading(false);
}
} }
}; };
loadIndexData(); loadIndexData();
// 组件卸载时清理
return () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
};
}, [wasmModule]); }, [wasmModule]);
// 检查activeFilters变化 // 检查activeFilters变化

View File

@ -148,14 +148,15 @@ const GitProjectCollection: React.FC<GitProjectCollectionProps> = ({
setProjects(data.projects || []); setProjects(data.projects || []);
setPagination(data.pagination || { current: page, total: 1, hasNext: false, hasPrev: page > 1 }); setPagination(data.pagination || { current: page, total: 1, hasNext: false, hasPrev: page > 1 });
} catch (err) { } catch (err) {
// 如果是取消的请求,不显示错误
if (err instanceof Error && err.name === 'AbortError') {
return;
}
// 如果组件已卸载,不继续更新状态 // 如果组件已卸载,不继续更新状态
if (!isMountedRef.current) return; if (!isMountedRef.current) return;
// 如果是取消的请求,不显示错误
if (err instanceof Error && (err.name === 'AbortError' || err.message.includes('aborted'))) {
console.log('请求被取消:', err.message);
return;
}
console.error('请求错误:', err); console.error('请求错误:', err);
setError(err instanceof Error ? err.message : '未知错误'); setError(err instanceof Error ? err.message : '未知错误');
// 保持之前的项目列表,避免清空显示 // 保持之前的项目列表,避免清空显示
@ -164,10 +165,10 @@ const GitProjectCollection: React.FC<GitProjectCollectionProps> = ({
} }
} finally { } finally {
// 如果组件已卸载,不继续更新状态 // 如果组件已卸载,不继续更新状态
if (!isMountedRef.current) return; if (isMountedRef.current) {
setLoading(false);
setLoading(false); setIsPageChanging(false);
setIsPageChanging(false); }
} }
}, [platform, username, organization, token, perPage, url, projects.length]); }, [platform, username, organization, token, perPage, url, projects.length]);

View File

@ -152,7 +152,7 @@ const navSelectorClassName = "mr-4";
</nav> </nav>
<!-- 使用自定义主题切换组件 --> <!-- 使用自定义主题切换组件 -->
<div class="mt-1.5"> <div class="flex items-center">
<ThemeToggle className="group" /> <ThemeToggle className="group" />
</div> </div>
</div> </div>
@ -314,7 +314,7 @@ const navSelectorClassName = "mr-4";
>切换主题</span >切换主题</span
> >
<div <div
class="group relative w-7 h-7 mt-1 flex items-center justify-center" class="group relative w-7 h-7 flex items-center justify-center"
> >
<ThemeToggle <ThemeToggle
width={14} width={14}

View File

@ -23,6 +23,8 @@ const MediaGrid: React.FC<MediaGridProps> = ({ type, doubanId }) => {
const abortControllerRef = useRef<AbortController | null>(null); const abortControllerRef = useRef<AbortController | null>(null);
const observerRef = useRef<IntersectionObserver | null>(null); const observerRef = useRef<IntersectionObserver | null>(null);
const scrollDetectorRef = useRef<HTMLDivElement | null>(null); const scrollDetectorRef = useRef<HTMLDivElement | null>(null);
// 添加一个 ref 来标记组件是否已挂载
const isMountedRef = useRef<boolean>(true);
// 使用ref来跟踪关键状态避免闭包问题 // 使用ref来跟踪关键状态避免闭包问题
const stateRef = useRef({ const stateRef = useRef({
@ -67,6 +69,12 @@ const MediaGrid: React.FC<MediaGridProps> = ({ type, doubanId }) => {
`/api/douban?type=${type}&start=${start}&doubanId=${doubanId}`, `/api/douban?type=${type}&start=${start}&doubanId=${doubanId}`,
{ signal: abortControllerRef.current.signal } { signal: abortControllerRef.current.signal }
); );
// 检查组件是否已卸载,如果卸载则不继续处理
if (!isMountedRef.current) {
return;
}
if (!response.ok) { if (!response.ok) {
// 解析响应内容,获取详细错误信息 // 解析响应内容,获取详细错误信息
let errorMessage = `获取${type === "movie" ? "电影" : "图书"}数据失败`; let errorMessage = `获取${type === "movie" ? "电影" : "图书"}数据失败`;
@ -112,6 +120,11 @@ const MediaGrid: React.FC<MediaGridProps> = ({ type, doubanId }) => {
const data = await response.json(); const data = await response.json();
// 再次检查组件是否已卸载
if (!isMountedRef.current) {
return;
}
if (data.items.length === 0) { if (data.items.length === 0) {
// 如果返回的项目为空,则认为已经没有更多内容 // 如果返回的项目为空,则认为已经没有更多内容
setHasMoreContent(false); setHasMoreContent(false);
@ -138,8 +151,17 @@ const MediaGrid: React.FC<MediaGridProps> = ({ type, doubanId }) => {
stateRef.current.hasMoreContent = newHasMoreContent; stateRef.current.hasMoreContent = newHasMoreContent;
} }
} catch (error) { } catch (error) {
// 检查组件是否已卸载
if (!isMountedRef.current) {
return;
}
// 如果是取消的请求,不显示错误 // 如果是取消的请求,不显示错误
if (error instanceof Error && error.name === 'AbortError') { if (error instanceof Error && error.name === 'AbortError') {
console.log('请求被取消', error.message);
// 如果是取消请求,重置加载状态但不显示错误
setIsLoading(false);
stateRef.current.isLoading = false;
return; return;
} }
@ -148,9 +170,12 @@ const MediaGrid: React.FC<MediaGridProps> = ({ type, doubanId }) => {
setItems([]); setItems([]);
} }
} finally { } finally {
// 重置加载状态 // 检查组件是否已卸载
setIsLoading(false); if (isMountedRef.current) {
stateRef.current.isLoading = false; // 重置加载状态
setIsLoading(false);
stateRef.current.isLoading = false;
}
} }
}, [type, doubanId]); }, [type, doubanId]);
@ -256,6 +281,9 @@ const MediaGrid: React.FC<MediaGridProps> = ({ type, doubanId }) => {
// 组件初始化和依赖变化时重置 // 组件初始化和依赖变化时重置
useEffect(() => { useEffect(() => {
// 设置组件挂载状态
isMountedRef.current = true;
// 重置状态 // 重置状态
setCurrentPage(1); setCurrentPage(1);
stateRef.current.currentPage = 1; stateRef.current.currentPage = 1;
@ -295,6 +323,9 @@ const MediaGrid: React.FC<MediaGridProps> = ({ type, doubanId }) => {
// 清理函数 // 清理函数
return () => { return () => {
// 标记组件已卸载
isMountedRef.current = false;
clearTimeout(timeoutId); clearTimeout(timeoutId);
window.removeEventListener("scroll", handleScroll); window.removeEventListener("scroll", handleScroll);

View File

@ -115,6 +115,10 @@ const Search: React.FC<SearchProps> = ({
const inlineSuggestionRef = useRef<HTMLDivElement>(null); const inlineSuggestionRef = useRef<HTMLDivElement>(null);
const observerRef = useRef<IntersectionObserver | null>(null); const observerRef = useRef<IntersectionObserver | null>(null);
const loadMoreRef = useRef<HTMLDivElement>(null); const loadMoreRef = useRef<HTMLDivElement>(null);
// 添加 AbortController 引用以取消请求
const abortControllerRef = useRef<AbortController | null>(null);
// 添加组件挂载状态引用
const isMountedRef = useRef<boolean>(true);
// 辅助函数 - 从loadingState获取各种加载状态 // 辅助函数 - 从loadingState获取各种加载状态
const isLoading = loadingState.status === 'loading_search'; const isLoading = loadingState.status === 'loading_search';
@ -135,8 +139,14 @@ const Search: React.FC<SearchProps> = ({
await wasm.default(); await wasm.default();
} }
// 检查组件是否仍然挂载
if (!isMountedRef.current) return;
setWasmModule(wasm as unknown as SearchWasm); setWasmModule(wasm as unknown as SearchWasm);
} catch (err) { } catch (err) {
// 检查组件是否仍然挂载
if (!isMountedRef.current) return;
console.error("加载搜索WASM模块失败:", err); console.error("加载搜索WASM模块失败:", err);
setLoadingState({ setLoadingState({
status: 'error', status: 'error',
@ -146,6 +156,11 @@ const Search: React.FC<SearchProps> = ({
}; };
loadWasmModule(); loadWasmModule();
// 组件卸载时清理
return () => {
isMountedRef.current = false;
};
}, []); }, []);
// 加载搜索索引 // 加载搜索索引
@ -153,10 +168,24 @@ const Search: React.FC<SearchProps> = ({
if (!wasmModule) return; if (!wasmModule) return;
const loadSearchIndex = async () => { const loadSearchIndex = async () => {
// 取消之前的请求
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
// 创建新的 AbortController
abortControllerRef.current = new AbortController();
try { try {
setLoadingState(prev => ({ ...prev, status: 'loading_index' })); setLoadingState(prev => ({ ...prev, status: 'loading_index' }));
const response = await fetch("/index/search_index.bin"); const response = await fetch("/index/search_index.bin", {
signal: abortControllerRef.current.signal
});
// 检查组件是否仍然挂载
if (!isMountedRef.current) return;
if (!response.ok) { if (!response.ok) {
throw new Error(`获取搜索索引失败: ${response.statusText}`); throw new Error(`获取搜索索引失败: ${response.statusText}`);
} }
@ -164,9 +193,21 @@ const Search: React.FC<SearchProps> = ({
const indexBuffer = await response.arrayBuffer(); const indexBuffer = await response.arrayBuffer();
const data = new Uint8Array(indexBuffer); const data = new Uint8Array(indexBuffer);
// 检查组件是否仍然挂载
if (!isMountedRef.current) return;
setIndexData(data); setIndexData(data);
setLoadingState(prev => ({ ...prev, status: 'success' })); setLoadingState(prev => ({ ...prev, status: 'success' }));
} catch (err) { } catch (err) {
// 检查组件是否仍然挂载
if (!isMountedRef.current) return;
// 如果是取消的请求,不显示错误
if (err instanceof Error && (err.name === 'AbortError' || err.message.includes('aborted'))) {
console.log('索引加载请求被取消:', err.message);
return;
}
console.error("搜索索引加载失败:", err); console.error("搜索索引加载失败:", err);
setLoadingState({ setLoadingState({
status: 'error', status: 'error',
@ -176,6 +217,13 @@ const Search: React.FC<SearchProps> = ({
}; };
loadSearchIndex(); loadSearchIndex();
// 组件卸载时清理
return () => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
};
}, [wasmModule]); }, [wasmModule]);
// 监听窗口大小变化,确保内联建议位置正确 // 监听窗口大小变化,确保内联建议位置正确
@ -264,6 +312,10 @@ const Search: React.FC<SearchProps> = ({
}; };
const result = wasmModule.search_articles(indexData, JSON.stringify(req)); const result = wasmModule.search_articles(indexData, JSON.stringify(req));
// 检查组件是否仍然挂载
if (!isMountedRef.current) return;
if (!result || result.trim() === "") { if (!result || result.trim() === "") {
setSuggestions([]); setSuggestions([]);
setInlineSuggestion(prev => ({ ...prev, visible: false })); setInlineSuggestion(prev => ({ ...prev, visible: false }));
@ -273,6 +325,9 @@ const Search: React.FC<SearchProps> = ({
const searchResult = JSON.parse(result) as SearchResult; const searchResult = JSON.parse(result) as SearchResult;
// 检查组件是否仍然挂载
if (!isMountedRef.current) return;
// 确保有suggestions字段且是数组 // 确保有suggestions字段且是数组
if (!searchResult?.suggestions || !Array.isArray(searchResult.suggestions) || searchResult.suggestions.length === 0) { if (!searchResult?.suggestions || !Array.isArray(searchResult.suggestions) || searchResult.suggestions.length === 0) {
setSuggestions([]); setSuggestions([]);
@ -305,6 +360,9 @@ const Search: React.FC<SearchProps> = ({
setInlineSuggestion(prev => ({ ...prev, visible: false })); setInlineSuggestion(prev => ({ ...prev, visible: false }));
} }
} catch (err) { } catch (err) {
// 检查组件是否仍然挂载
if (!isMountedRef.current) return;
console.error("获取内联建议失败:", err); console.error("获取内联建议失败:", err);
setInlineSuggestion(prev => ({ ...prev, visible: false })); setInlineSuggestion(prev => ({ ...prev, visible: false }));
setSelectedSuggestionIndex(0); // 重置选中索引 setSelectedSuggestionIndex(0); // 重置选中索引
@ -524,6 +582,14 @@ const Search: React.FC<SearchProps> = ({
return; return;
} }
// 取消之前的请求虽然这是WASM调用不是真正的网络请求但保持一致性
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
// 创建新的 AbortController
abortControllerRef.current = new AbortController();
try { try {
const page = isLoadMore ? currentPage : 1; const page = isLoadMore ? currentPage : 1;
@ -544,6 +610,9 @@ const Search: React.FC<SearchProps> = ({
const resultJson = wasmModule.search_articles(indexData, JSON.stringify(req)); const resultJson = wasmModule.search_articles(indexData, JSON.stringify(req));
// 检查组件是否仍然挂载
if (!isMountedRef.current) return;
if (!resultJson || resultJson.trim() === "") { if (!resultJson || resultJson.trim() === "") {
console.error("返回的搜索结果为空"); console.error("返回的搜索结果为空");
setLoadingState({ setLoadingState({
@ -555,6 +624,9 @@ const Search: React.FC<SearchProps> = ({
const result = JSON.parse(resultJson) as SearchResult; const result = JSON.parse(resultJson) as SearchResult;
// 检查组件是否仍然挂载
if (!isMountedRef.current) return;
// 预处理搜索结果 // 预处理搜索结果
for (const item of result.items) { for (const item of result.items) {
if (item.heading_tree) { if (item.heading_tree) {
@ -584,6 +656,9 @@ const Search: React.FC<SearchProps> = ({
// 更新加载状态 // 更新加载状态
setLoadingState(prev => ({ ...prev, status: 'success' })); setLoadingState(prev => ({ ...prev, status: 'success' }));
} catch (err) { } catch (err) {
// 检查组件是否仍然挂载
if (!isMountedRef.current) return;
console.error("搜索执行失败:", err); console.error("搜索执行失败:", err);
setLoadingState({ setLoadingState({
status: 'error', status: 'error',
@ -968,6 +1043,35 @@ const Search: React.FC<SearchProps> = ({
); );
}; };
// 组件卸载时清理
useEffect(() => {
// 设置组件已挂载状态
isMountedRef.current = true;
return () => {
// 标记组件已卸载
isMountedRef.current = false;
// 清理所有定时器
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}
if (searchTimerRef.current) {
clearTimeout(searchTimerRef.current);
}
// 取消所有进行中的请求
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
// 清理观察器
if (observerRef.current) {
observerRef.current.disconnect();
}
};
}, []);
// 渲染结束 // 渲染结束
const returnBlock = ( const returnBlock = (
<div className="relative [&_mark]:bg-yellow-200 dark:[&_mark]:bg-yellow-800"> <div className="relative [&_mark]:bg-yellow-200 dark:[&_mark]:bg-yellow-800">

View File

@ -19,6 +19,22 @@ import {
} from "three"; } from "three";
import type { Side } from "three"; import type { Side } from "three";
// 为requestIdleCallback添加类型声明
interface RequestIdleCallbackOptions {
timeout: number;
}
interface Window {
requestIdleCallback?: (
callback: (deadline: {
didTimeout: boolean;
timeRemaining: () => number;
}) => void,
opts?: RequestIdleCallbackOptions
) => number;
cancelIdleCallback?: (handle: number) => void;
}
// 需要懒加载的模块 // 需要懒加载的模块
const loadControlsAndRenderers = () => Promise.all([ const loadControlsAndRenderers = () => Promise.all([
import("three/examples/jsm/controls/OrbitControls.js"), import("three/examples/jsm/controls/OrbitControls.js"),
@ -129,17 +145,32 @@ const WorldHeatmap: React.FC<WorldHeatmapProps> = ({ visitedPlaces }) => {
// 从公共目录加载地图数据 // 从公共目录加载地图数据
useEffect(() => { useEffect(() => {
// 创建 AbortController 用于在组件卸载时取消请求
const controller = new AbortController();
const signal = controller.signal;
const loadMapData = async () => { const loadMapData = async () => {
try { try {
setMapLoading(true); setMapLoading(true);
setMapError(null); setMapError(null);
// 从公共目录加载地图数据 // 添加请求超时处理
const timeout = setTimeout(() => controller.abort(), 30000); // 30秒超时
// 从公共目录加载地图数据,并使用 signal 和缓存控制
const [worldDataResponse, chinaDataResponse] = await Promise.all([ const [worldDataResponse, chinaDataResponse] = await Promise.all([
fetch('/maps/world.zh.json'), fetch('/maps/world.zh.json', {
fetch('/maps/china.json') signal,
headers: { 'Cache-Control': 'no-cache' }
}),
fetch('/maps/china.json', {
signal,
headers: { 'Cache-Control': 'no-cache' }
})
]); ]);
clearTimeout(timeout);
if (!worldDataResponse.ok || !chinaDataResponse.ok) { if (!worldDataResponse.ok || !chinaDataResponse.ok) {
throw new Error('无法获取地图数据'); throw new Error('无法获取地图数据');
} }
@ -153,15 +184,23 @@ const WorldHeatmap: React.FC<WorldHeatmapProps> = ({ visitedPlaces }) => {
}); });
setMapLoading(false); setMapLoading(false);
} catch (err) { } catch (err: any) {
console.error("加载地图数据失败:", err); // 只有当请求不是被我们自己中断时才设置错误状态
const errorMessage = err instanceof Error ? err.message : String(err); if (err.name !== 'AbortError') {
setMapError(`地图数据加载失败: ${errorMessage}`); console.error("加载地图数据失败:", err);
const errorMessage = err instanceof Error ? err.message : String(err);
setMapError(`地图数据加载失败: ${errorMessage}`);
}
setMapLoading(false); setMapLoading(false);
} }
}; };
loadMapData(); loadMapData();
// 清理函数:组件卸载时中断请求
return () => {
controller.abort();
};
}, []); }, []);
// 加载WASM模块 // 加载WASM模块
@ -177,7 +216,7 @@ const WorldHeatmap: React.FC<WorldHeatmapProps> = ({ visitedPlaces }) => {
} }
setWasmModule(wasm as unknown as GeoWasmModule); setWasmModule(wasm as unknown as GeoWasmModule);
setWasmError(null); setWasmError(null);
} catch (err) { } catch (err: any) {
console.error("加载WASM模块失败:", err); console.error("加载WASM模块失败:", err);
const errorMessage = err instanceof Error ? err.message : String(err); const errorMessage = err instanceof Error ? err.message : String(err);
setWasmError(`WASM模块初始化失败: ${errorMessage}`); setWasmError(`WASM模块初始化失败: ${errorMessage}`);
@ -203,7 +242,7 @@ const WorldHeatmap: React.FC<WorldHeatmapProps> = ({ visitedPlaces }) => {
setGeoProcessor(geoProcessorInstance); setGeoProcessor(geoProcessorInstance);
setWasmReady(true); setWasmReady(true);
} catch (error) { } catch (error: any) {
console.error("WASM数据处理失败:", error); console.error("WASM数据处理失败:", error);
const errorMessage = error instanceof Error ? error.message : String(error); const errorMessage = error instanceof Error ? error.message : String(error);
setWasmError(`WASM数据处理失败: ${errorMessage}`); setWasmError(`WASM数据处理失败: ${errorMessage}`);
@ -689,10 +728,41 @@ const WorldHeatmap: React.FC<WorldHeatmapProps> = ({ visitedPlaces }) => {
return null; return null;
}; };
// 化的动画循环函数 // 化的动画循环函数
const animate = () => { const animate = () => {
if (!sceneRef.current) return; if (!sceneRef.current) return;
// 使用requestIdleCallback如果可用或setTimeout来限制更新频率
// 这样可以减少CPU使用率和提高性能
const scheduleNextFrame = () => {
if (typeof window.requestIdleCallback === 'function') {
window.requestIdleCallback(() => {
if (sceneRef.current) {
sceneRef.current.animationId = requestAnimationFrame(animate);
}
}, { timeout: 100 });
} else {
setTimeout(() => {
if (sceneRef.current) {
sceneRef.current.animationId = requestAnimationFrame(animate);
}
}, 16); // 约60fps的更新频率
}
};
// 如果相机没有变化,可以降低渲染频率
if (sceneRef.current.controls &&
!sceneRef.current.controls.autoRotate &&
sceneRef.current.lastCameraPosition &&
sceneRef.current.camera.position.distanceTo(sceneRef.current.lastCameraPosition) < 0.001) {
// 相机位置没有明显变化,降低渲染频率
scheduleNextFrame();
return;
}
// 保存当前相机位置
sceneRef.current.lastCameraPosition = sceneRef.current.camera.position.clone();
// 更新控制器 // 更新控制器
controls.update(); controls.update();
@ -700,8 +770,8 @@ const WorldHeatmap: React.FC<WorldHeatmapProps> = ({ visitedPlaces }) => {
renderer.render(scene, camera); renderer.render(scene, camera);
labelRenderer.render(scene, camera); labelRenderer.render(scene, camera);
// 请求下一帧 // 安排下一帧,使用优化的调度方式
sceneRef.current.animationId = requestAnimationFrame(animate); scheduleNextFrame();
}; };
// 处理窗口大小变化 // 处理窗口大小变化
@ -751,8 +821,10 @@ const WorldHeatmap: React.FC<WorldHeatmapProps> = ({ visitedPlaces }) => {
// 开始动画 // 开始动画
sceneRef.current.animationId = requestAnimationFrame(animate); sceneRef.current.animationId = requestAnimationFrame(animate);
} catch (error) { } catch (error: any) {
console.error("Three.js初始化失败:", error); console.error("Three.js初始化失败:", error);
const errorMessage = error instanceof Error ? error.message : String(error);
setMapError(`3D地图初始化失败: ${errorMessage}`);
} }
}; };

View File

@ -26,19 +26,47 @@ function delay(ms: number) {
// 带超时的 fetch 函数 // 带超时的 fetch 函数
async function fetchWithTimeout(url: string, options: RequestInit, timeoutMs: number) { async function fetchWithTimeout(url: string, options: RequestInit, timeoutMs: number) {
const controller = new AbortController(); // 检查是否已经提供了信号
const { signal } = controller; const existingSignal = options.signal;
// 创建我们自己的 AbortController 用于超时
const timeoutController = new AbortController();
const timeoutSignal = timeoutController.signal;
// 设置超时
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
controller.abort(); timeoutController.abort();
}, timeoutMs); }, timeoutMs);
try { try {
const response = await fetch(url, { // 使用已有的信号和我们的超时信号
...options, if (existingSignal) {
signal // 如果已经取消了,直接抛出异常
}); if (existingSignal.aborted) {
return response; throw new DOMException('已被用户取消', 'AbortError');
}
// 创建一个监听器,当外部信号中止时,也中止我们的控制器
const abortListener = () => timeoutController.abort();
existingSignal.addEventListener('abort', abortListener);
// 进行请求,但只使用我们的超时信号
const response = await fetch(url, {
...options,
signal: timeoutSignal
});
// 移除监听器
existingSignal.removeEventListener('abort', abortListener);
return response;
} else {
// 如果没有提供信号,只使用我们的超时信号
return await fetch(url, {
...options,
signal: timeoutSignal
});
}
} finally { } finally {
clearTimeout(timeout); clearTimeout(timeout);
} }
@ -314,6 +342,24 @@ export const GET: APIRoute = async ({ request }) => {
}); });
} catch (error) { } catch (error) {
console.error(`尝试第 ${retries + 1}/${MAX_RETRIES + 1} 次失败:`, error); console.error(`尝试第 ${retries + 1}/${MAX_RETRIES + 1} 次失败:`, error);
// 判断是否是请求被中止
if (error instanceof Error && (error.name === 'AbortError' || error.message.includes('aborted'))) {
console.warn('请求被中止:', error.message);
// 对于中止请求我们可以直接返回404
return new Response(JSON.stringify({
error: '请求被中止',
message: '请求已被用户或服务器中止',
status: 499 // 使用499代表客户端中止请求
}), {
status: 499,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-store, max-age=0'
}
});
}
lastError = error instanceof Error ? error : new Error(String(error)); lastError = error instanceof Error ? error : new Error(String(error));
if (retries < MAX_RETRIES) { if (retries < MAX_RETRIES) {
@ -332,6 +378,21 @@ export const GET: APIRoute = async ({ request }) => {
// 检查是否是常见错误类型并返回对应错误信息 // 检查是否是常见错误类型并返回对应错误信息
const errorMessage = lastError?.message || '未知错误'; const errorMessage = lastError?.message || '未知错误';
// 检查是否是中止错误
if (lastError && (lastError.name === 'AbortError' || errorMessage.includes('aborted'))) {
return new Response(JSON.stringify({
error: '请求被中止',
message: '请求已被用户或系统中止',
status: 499
}), {
status: 499,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-store, max-age=0'
}
});
}
// 根据错误信息判断错误类型 // 根据错误信息判断错误类型
if (errorMessage.includes('403') || errorMessage.includes('禁止访问') || errorMessage.includes('频繁')) { if (errorMessage.includes('403') || errorMessage.includes('禁止访问') || errorMessage.includes('频繁')) {
return new Response(JSON.stringify({ return new Response(JSON.stringify({
@ -387,6 +448,22 @@ export const GET: APIRoute = async ({ request }) => {
}); });
} catch (error) { } catch (error) {
console.error('处理请求时出错:', error); console.error('处理请求时出错:', error);
// 判断是否是中止错误
if (error instanceof Error && (error.name === 'AbortError' || error.message.includes('aborted'))) {
return new Response(JSON.stringify({
error: '请求被中止',
message: '请求已被用户或系统中止',
status: 499
}), {
status: 499,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-store, max-age=0'
}
});
}
return new Response(JSON.stringify({ return new Response(JSON.stringify({
error: '获取豆瓣数据失败', error: '获取豆瓣数据失败',
message: error instanceof Error ? error.message : '未知错误' message: error instanceof Error ? error.message : '未知错误'

View File

@ -87,9 +87,26 @@ export async function GET({ request }: APIContext) {
headers headers
}); });
} catch (error) { } catch (error) {
// 检查是否为请求中止错误
if (error instanceof Error && (error.name === 'AbortError' || error.message.includes('aborted'))) {
return new Response(JSON.stringify({
error: '请求被用户中止',
message: error.message,
type: 'abort'
}), {
status: 499, // 使用 499 状态码表示客户端关闭请求
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
}
});
}
// 处理其他类型的错误
return new Response(JSON.stringify({ return new Response(JSON.stringify({
error: '处理请求错误', error: '处理请求错误',
message: error instanceof Error ? error.message : '未知错误' message: error instanceof Error ? error.message : '未知错误',
type: 'server'
}), { }), {
status: 500, status: 500,
headers: { headers: {
@ -111,6 +128,112 @@ export function OPTIONS() {
}); });
} }
// 使用带超时和重试的 fetch 函数
async function fetchWithRetry(url: string, options: any, retries = 3, timeout = 10000) {
let lastError: Error | null = null;
for (let attempt = 0; attempt < retries; attempt++) {
try {
// 创建 AbortController 用于超时控制
const controller = new AbortController();
// 如果原始请求已有 signal保持追踪以便正确处理用户中止
const originalSignal = options?.signal;
// 设置超时定时器
const timeoutId = setTimeout(() => {
controller.abort(`请求超时 (${timeout}ms)`);
}, timeout);
// 添加超时的 signal 到请求选项
const fetchOptions = {
...options,
signal: controller.signal
};
// 如果有原始信号,监听其中止事件以便同步中止
if (originalSignal) {
if (originalSignal.aborted) {
// 如果原始信号已经被中止,立即中止当前请求
controller.abort('用户取消请求');
clearTimeout(timeoutId);
throw new Error('用户取消请求');
}
// 监听原始信号的中止事件
const abortHandler = () => {
controller.abort('用户取消请求');
clearTimeout(timeoutId);
};
originalSignal.addEventListener('abort', abortHandler);
// 确保在操作完成后清理事件监听器
setTimeout(() => {
try {
originalSignal.removeEventListener('abort', abortHandler);
} catch (e) {
// 忽略可能出现的清理错误
}
}, 0);
}
try {
const response = await fetch(url, fetchOptions);
clearTimeout(timeoutId);
return response;
} catch (error) {
clearTimeout(timeoutId);
// 检查是否为中止错误
if (error instanceof Error && (error.name === 'AbortError' || error.message.includes('aborted'))) {
// 确定中止原因 - 是超时还是用户请求
const isTimeout = error.message.includes('timeout') || error.message.includes('超时');
if (isTimeout && attempt < retries - 1) {
// 如果是超时且还有重试次数,继续重试
console.log(`请求超时,正在重试 (${attempt + 1}/${retries})...`);
lastError = error;
// 等待一段时间后重试
await new Promise(resolve => setTimeout(resolve, 1000 * (attempt + 1)));
continue;
} else if (!isTimeout) {
// 如果是用户主动中止,直接抛出错误
throw error;
}
}
// 其他错误情况
lastError = error as Error;
// 增加重试间隔
if (attempt < retries - 1) {
console.log(`请求失败,正在重试 (${attempt + 1}/${retries})...`);
await new Promise(resolve => setTimeout(resolve, 1000 * (attempt + 1)));
continue;
}
throw lastError;
}
} catch (error) {
lastError = error as Error;
// 如果是中止错误,直接抛出不再重试
if (error instanceof Error && (error.name === 'AbortError' || error.message.includes('aborted'))) {
throw error;
}
// 最后一次尝试失败
if (attempt === retries - 1) {
throw lastError;
}
}
}
// 所有重试都失败了
throw lastError || new Error('所有重试请求都失败了');
}
async function fetchGithubProjects(username: string, organization: string, page: number, config: any) { async function fetchGithubProjects(username: string, organization: string, page: number, config: any) {
const maxRetries = 3; const maxRetries = 3;
let retryCount = 0; let retryCount = 0;
@ -193,6 +316,11 @@ async function fetchGithubProjects(username: string, organization: string, page:
} }
}; };
} catch (error) { } catch (error) {
// 检查是否为中止错误
if (error instanceof Error && (error.name === 'AbortError' || error.message.includes('aborted'))) {
throw error; // 中止错误直接抛出,不重试
}
retryCount++; retryCount++;
if (retryCount >= maxRetries) { if (retryCount >= maxRetries) {
@ -218,6 +346,7 @@ async function fetchGiteaProjects(username: string, organization: string, page:
try { try {
const perPage = config.perPage || 10; const perPage = config.perPage || 10;
const giteaUrl = config.url; const giteaUrl = config.url;
const signal = config.signal; // 获取可能的 AbortSignal
if (!giteaUrl) { if (!giteaUrl) {
throw new Error('Gitea URL 不存在'); throw new Error('Gitea URL 不存在');
@ -232,12 +361,13 @@ async function fetchGiteaProjects(username: string, organization: string, page:
apiUrl = `${giteaUrl}/api/v1/users/${config.username}/repos?page=${page}&per_page=${perPage}`; apiUrl = `${giteaUrl}/api/v1/users/${config.username}/repos?page=${page}&per_page=${perPage}`;
} }
const response = await fetch(apiUrl, { const response = await fetchWithRetry(apiUrl, {
headers: { headers: {
'Accept': 'application/json', 'Accept': 'application/json',
...(config.token ? { 'Authorization': `token ${config.token}` } : {}) ...(config.token ? { 'Authorization': `token ${config.token}` } : {})
} },
}); signal // 传递 AbortSignal
}, 3, 15000); // 最多重试3次每次超时15秒
if (!response.ok) { if (!response.ok) {
throw new Error(`Gitea API 请求失败: ${response.statusText}`); throw new Error(`Gitea API 请求失败: ${response.statusText}`);
@ -273,6 +403,13 @@ async function fetchGiteaProjects(username: string, organization: string, page:
} }
}; };
} catch (error) { } catch (error) {
// 检查是否为中止错误,将其向上传播
if (error instanceof Error && (error.name === 'AbortError' || error.message.includes('aborted'))) {
throw error;
}
console.error('获取 Gitea 项目失败:', error);
return { return {
projects: [], projects: [],
pagination: { pagination: {
@ -288,6 +425,7 @@ async function fetchGiteaProjects(username: string, organization: string, page:
async function fetchGiteeProjects(username: string, organization: string, page: number, config: any) { async function fetchGiteeProjects(username: string, organization: string, page: number, config: any) {
try { try {
const perPage = config.perPage || 10; const perPage = config.perPage || 10;
const signal = config.signal; // 获取可能的 AbortSignal
const giteeUsername = username || config.username; const giteeUsername = username || config.username;
@ -306,7 +444,9 @@ async function fetchGiteeProjects(username: string, organization: string, page:
apiUrl += `&access_token=${config.token}`; apiUrl += `&access_token=${config.token}`;
} }
const response = await fetch(apiUrl); const response = await fetchWithRetry(apiUrl, {
signal // 传递 AbortSignal
}, 3, 15000); // 最多重试3次每次超时15秒
if (!response.ok) { if (!response.ok) {
throw new Error(`Gitee API 请求失败: ${response.statusText}`); throw new Error(`Gitee API 请求失败: ${response.statusText}`);
@ -340,6 +480,13 @@ async function fetchGiteeProjects(username: string, organization: string, page:
} }
}; };
} catch (error) { } catch (error) {
// 检查是否为中止错误,将其向上传播
if (error instanceof Error && (error.name === 'AbortError' || error.message.includes('aborted'))) {
throw error;
}
console.error('获取 Gitee 项目失败:', error);
return { return {
projects: [], projects: [],
pagination: { pagination: {