增加请求错误处理
This commit is contained in:
parent
d3e0eddff7
commit
1336dc9081
@ -547,9 +547,23 @@ const ArticleFilter: React.FC<ArticleFilterProps> = ({ searchParams = {} }) => {
|
||||
// 添加客户端标记变量,确保只在客户端渲染某些组件
|
||||
const [isClient, setIsClient] = useState(false);
|
||||
|
||||
// 添加 AbortController 引用和组件挂载状态引用
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
const isMountedRef = useRef<boolean>(true);
|
||||
|
||||
// 组件挂载时设置客户端标记
|
||||
useEffect(() => {
|
||||
setIsClient(true);
|
||||
|
||||
// 组件卸载时的清理
|
||||
return () => {
|
||||
isMountedRef.current = false;
|
||||
|
||||
// 取消进行中的请求
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 处理searchParams,确保我们有正确的参数格式
|
||||
@ -692,6 +706,9 @@ const ArticleFilter: React.FC<ArticleFilterProps> = ({ searchParams = {} }) => {
|
||||
const filterParamsJson = JSON.stringify(filterParams);
|
||||
const result = await wasmModule.ArticleFilterJS.filter_articles(filterParamsJson);
|
||||
|
||||
// 检查组件是否仍然挂载
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
// 处理结果
|
||||
if (!result || typeof result !== 'object') {
|
||||
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 totalPages = typeof result.total_pages === 'number' ? result.total_pages :
|
||||
Math.ceil(total / filters.pageSize);
|
||||
Math.ceil(total / filters.pageSize);
|
||||
|
||||
// 更新状态时提供明确的默认值
|
||||
setFilteredArticles(articles);
|
||||
@ -732,10 +752,16 @@ const ArticleFilter: React.FC<ArticleFilterProps> = ({ searchParams = {} }) => {
|
||||
setTotalPages(totalPages || 1);
|
||||
|
||||
} catch (error) {
|
||||
// 检查组件是否仍然挂载
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
console.error("[筛选] 应用筛选逻辑出错:", error);
|
||||
setError("筛选文章时出错,请刷新页面重试");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
// 检查组件是否仍然挂载
|
||||
if (isMountedRef.current) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -746,11 +772,22 @@ const ArticleFilter: React.FC<ArticleFilterProps> = ({ searchParams = {} }) => {
|
||||
const wasm = await import(
|
||||
"@/assets/wasm/article-filter/article_filter.js"
|
||||
);
|
||||
|
||||
// 检查组件是否仍然挂载
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
if (typeof wasm.default === "function") {
|
||||
await wasm.default();
|
||||
}
|
||||
|
||||
// 再次检查组件是否仍然挂载
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
setWasmModule(wasm as unknown as ArticleFilterWasm);
|
||||
} catch (err) {
|
||||
// 检查组件是否仍然挂载
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
console.error("加载WASM模块失败:", err);
|
||||
setError("加载筛选模块失败,请刷新页面重试");
|
||||
}
|
||||
@ -766,38 +803,79 @@ const ArticleFilter: React.FC<ArticleFilterProps> = ({ searchParams = {} }) => {
|
||||
|
||||
|
||||
const loadIndexData = async () => {
|
||||
// 取消之前的请求
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
}
|
||||
|
||||
// 创建新的 AbortController
|
||||
abortControllerRef.current = new AbortController();
|
||||
|
||||
try {
|
||||
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) {
|
||||
throw new Error(`获取筛选索引失败: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const indexData = await response.arrayBuffer();
|
||||
|
||||
// 检查组件是否仍然挂载
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
// 初始化WASM模块
|
||||
try {
|
||||
await wasmModule.ArticleFilterJS.init(new Uint8Array(indexData));
|
||||
|
||||
// 检查组件是否仍然挂载
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
// 获取所有标签
|
||||
const tags = (await wasmModule.ArticleFilterJS.get_all_tags()) || [];
|
||||
|
||||
// 检查组件是否仍然挂载
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
setAllAvailableTags(tags);
|
||||
|
||||
// 初始加载时不依赖applyFilters函数,而是直接执行筛选逻辑
|
||||
// 这避免了循环依赖问题
|
||||
await initialLoadArticles();
|
||||
|
||||
// 检查组件是否仍然挂载
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
setIsArticlesLoaded(true);
|
||||
} catch (parseError) {
|
||||
// 检查组件是否仍然挂载
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
console.error("解析筛选索引数据失败:", parseError);
|
||||
setError("索引文件存在但格式不正确,需要重新构建索引");
|
||||
}
|
||||
} 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);
|
||||
setError("索引文件缺失或无法读取,请重新构建索引");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
// 检查组件是否仍然挂载
|
||||
if (isMountedRef.current) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -830,6 +908,9 @@ const ArticleFilter: React.FC<ArticleFilterProps> = ({ searchParams = {} }) => {
|
||||
filterParamsJson,
|
||||
);
|
||||
|
||||
// 检查组件是否仍然挂载
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
// 检查结果格式
|
||||
if (!result || typeof result !== 'object') {
|
||||
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 totalPages = typeof result.total_pages === 'number' ? result.total_pages :
|
||||
@ -870,7 +954,7 @@ const ArticleFilter: React.FC<ArticleFilterProps> = ({ searchParams = {} }) => {
|
||||
setTotalPages(totalPages || 1);
|
||||
|
||||
// 只有在非初始加载时才更新URL参数
|
||||
if (!skipUrlUpdate) {
|
||||
if (!skipUrlUpdate && typeof window !== 'undefined') {
|
||||
// 初始化URL参数
|
||||
const params = new URLSearchParams();
|
||||
|
||||
@ -910,10 +994,16 @@ const ArticleFilter: React.FC<ArticleFilterProps> = ({ searchParams = {} }) => {
|
||||
|
||||
return result;
|
||||
} catch (wasmError) {
|
||||
// 检查组件是否仍然挂载
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
console.error("WASM执行失败");
|
||||
throw new Error(`WASM执行失败`);
|
||||
}
|
||||
} catch (error) {
|
||||
// 检查组件是否仍然挂载
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
console.error("初始加载文章出错");
|
||||
setError("加载文章时出错,请刷新页面重试");
|
||||
return {
|
||||
@ -924,11 +1014,21 @@ const ArticleFilter: React.FC<ArticleFilterProps> = ({ searchParams = {} }) => {
|
||||
total_pages: 0,
|
||||
};
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
// 检查组件是否仍然挂载
|
||||
if (isMountedRef.current) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadIndexData();
|
||||
|
||||
// 组件卸载时清理
|
||||
return () => {
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
}
|
||||
};
|
||||
}, [wasmModule]);
|
||||
|
||||
// 检查activeFilters变化
|
||||
|
@ -148,14 +148,15 @@ const GitProjectCollection: React.FC<GitProjectCollectionProps> = ({
|
||||
setProjects(data.projects || []);
|
||||
setPagination(data.pagination || { current: page, total: 1, hasNext: false, hasPrev: page > 1 });
|
||||
} catch (err) {
|
||||
// 如果是取消的请求,不显示错误
|
||||
if (err instanceof Error && err.name === 'AbortError') {
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果组件已卸载,不继续更新状态
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
// 如果是取消的请求,不显示错误
|
||||
if (err instanceof Error && (err.name === 'AbortError' || err.message.includes('aborted'))) {
|
||||
console.log('请求被取消:', err.message);
|
||||
return;
|
||||
}
|
||||
|
||||
console.error('请求错误:', err);
|
||||
setError(err instanceof Error ? err.message : '未知错误');
|
||||
// 保持之前的项目列表,避免清空显示
|
||||
@ -164,10 +165,10 @@ const GitProjectCollection: React.FC<GitProjectCollectionProps> = ({
|
||||
}
|
||||
} finally {
|
||||
// 如果组件已卸载,不继续更新状态
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
setLoading(false);
|
||||
setIsPageChanging(false);
|
||||
if (isMountedRef.current) {
|
||||
setLoading(false);
|
||||
setIsPageChanging(false);
|
||||
}
|
||||
}
|
||||
}, [platform, username, organization, token, perPage, url, projects.length]);
|
||||
|
||||
|
@ -152,7 +152,7 @@ const navSelectorClassName = "mr-4";
|
||||
</nav>
|
||||
|
||||
<!-- 使用自定义主题切换组件 -->
|
||||
<div class="mt-1.5">
|
||||
<div class="flex items-center">
|
||||
<ThemeToggle className="group" />
|
||||
</div>
|
||||
</div>
|
||||
@ -314,7 +314,7 @@ const navSelectorClassName = "mr-4";
|
||||
>切换主题</span
|
||||
>
|
||||
<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
|
||||
width={14}
|
||||
|
@ -23,6 +23,8 @@ const MediaGrid: React.FC<MediaGridProps> = ({ type, doubanId }) => {
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
const observerRef = useRef<IntersectionObserver | null>(null);
|
||||
const scrollDetectorRef = useRef<HTMLDivElement | null>(null);
|
||||
// 添加一个 ref 来标记组件是否已挂载
|
||||
const isMountedRef = useRef<boolean>(true);
|
||||
|
||||
// 使用ref来跟踪关键状态,避免闭包问题
|
||||
const stateRef = useRef({
|
||||
@ -67,6 +69,12 @@ const MediaGrid: React.FC<MediaGridProps> = ({ type, doubanId }) => {
|
||||
`/api/douban?type=${type}&start=${start}&doubanId=${doubanId}`,
|
||||
{ signal: abortControllerRef.current.signal }
|
||||
);
|
||||
|
||||
// 检查组件是否已卸载,如果卸载则不继续处理
|
||||
if (!isMountedRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
// 解析响应内容,获取详细错误信息
|
||||
let errorMessage = `获取${type === "movie" ? "电影" : "图书"}数据失败`;
|
||||
@ -112,6 +120,11 @@ const MediaGrid: React.FC<MediaGridProps> = ({ type, doubanId }) => {
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// 再次检查组件是否已卸载
|
||||
if (!isMountedRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.items.length === 0) {
|
||||
// 如果返回的项目为空,则认为已经没有更多内容
|
||||
setHasMoreContent(false);
|
||||
@ -138,8 +151,17 @@ const MediaGrid: React.FC<MediaGridProps> = ({ type, doubanId }) => {
|
||||
stateRef.current.hasMoreContent = newHasMoreContent;
|
||||
}
|
||||
} catch (error) {
|
||||
// 检查组件是否已卸载
|
||||
if (!isMountedRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果是取消的请求,不显示错误
|
||||
if (error instanceof Error && error.name === 'AbortError') {
|
||||
console.log('请求被取消', error.message);
|
||||
// 如果是取消请求,重置加载状态但不显示错误
|
||||
setIsLoading(false);
|
||||
stateRef.current.isLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@ -148,9 +170,12 @@ const MediaGrid: React.FC<MediaGridProps> = ({ type, doubanId }) => {
|
||||
setItems([]);
|
||||
}
|
||||
} finally {
|
||||
// 重置加载状态
|
||||
setIsLoading(false);
|
||||
stateRef.current.isLoading = false;
|
||||
// 检查组件是否已卸载
|
||||
if (isMountedRef.current) {
|
||||
// 重置加载状态
|
||||
setIsLoading(false);
|
||||
stateRef.current.isLoading = false;
|
||||
}
|
||||
}
|
||||
}, [type, doubanId]);
|
||||
|
||||
@ -256,6 +281,9 @@ const MediaGrid: React.FC<MediaGridProps> = ({ type, doubanId }) => {
|
||||
|
||||
// 组件初始化和依赖变化时重置
|
||||
useEffect(() => {
|
||||
// 设置组件挂载状态
|
||||
isMountedRef.current = true;
|
||||
|
||||
// 重置状态
|
||||
setCurrentPage(1);
|
||||
stateRef.current.currentPage = 1;
|
||||
@ -295,6 +323,9 @@ const MediaGrid: React.FC<MediaGridProps> = ({ type, doubanId }) => {
|
||||
|
||||
// 清理函数
|
||||
return () => {
|
||||
// 标记组件已卸载
|
||||
isMountedRef.current = false;
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
window.removeEventListener("scroll", handleScroll);
|
||||
|
||||
|
@ -115,6 +115,10 @@ const Search: React.FC<SearchProps> = ({
|
||||
const inlineSuggestionRef = useRef<HTMLDivElement>(null);
|
||||
const observerRef = useRef<IntersectionObserver | null>(null);
|
||||
const loadMoreRef = useRef<HTMLDivElement>(null);
|
||||
// 添加 AbortController 引用以取消请求
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
// 添加组件挂载状态引用
|
||||
const isMountedRef = useRef<boolean>(true);
|
||||
|
||||
// 辅助函数 - 从loadingState获取各种加载状态
|
||||
const isLoading = loadingState.status === 'loading_search';
|
||||
@ -135,8 +139,14 @@ const Search: React.FC<SearchProps> = ({
|
||||
await wasm.default();
|
||||
}
|
||||
|
||||
// 检查组件是否仍然挂载
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
setWasmModule(wasm as unknown as SearchWasm);
|
||||
} catch (err) {
|
||||
// 检查组件是否仍然挂载
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
console.error("加载搜索WASM模块失败:", err);
|
||||
setLoadingState({
|
||||
status: 'error',
|
||||
@ -146,6 +156,11 @@ const Search: React.FC<SearchProps> = ({
|
||||
};
|
||||
|
||||
loadWasmModule();
|
||||
|
||||
// 组件卸载时清理
|
||||
return () => {
|
||||
isMountedRef.current = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 加载搜索索引
|
||||
@ -153,10 +168,24 @@ const Search: React.FC<SearchProps> = ({
|
||||
if (!wasmModule) return;
|
||||
|
||||
const loadSearchIndex = async () => {
|
||||
// 取消之前的请求
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
}
|
||||
|
||||
// 创建新的 AbortController
|
||||
abortControllerRef.current = new AbortController();
|
||||
|
||||
try {
|
||||
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) {
|
||||
throw new Error(`获取搜索索引失败: ${response.statusText}`);
|
||||
}
|
||||
@ -164,9 +193,21 @@ const Search: React.FC<SearchProps> = ({
|
||||
const indexBuffer = await response.arrayBuffer();
|
||||
const data = new Uint8Array(indexBuffer);
|
||||
|
||||
// 检查组件是否仍然挂载
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
setIndexData(data);
|
||||
setLoadingState(prev => ({ ...prev, status: 'success' }));
|
||||
} 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);
|
||||
setLoadingState({
|
||||
status: 'error',
|
||||
@ -176,6 +217,13 @@ const Search: React.FC<SearchProps> = ({
|
||||
};
|
||||
|
||||
loadSearchIndex();
|
||||
|
||||
// 组件卸载时清理
|
||||
return () => {
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
}
|
||||
};
|
||||
}, [wasmModule]);
|
||||
|
||||
// 监听窗口大小变化,确保内联建议位置正确
|
||||
@ -264,6 +312,10 @@ const Search: React.FC<SearchProps> = ({
|
||||
};
|
||||
|
||||
const result = wasmModule.search_articles(indexData, JSON.stringify(req));
|
||||
|
||||
// 检查组件是否仍然挂载
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
if (!result || result.trim() === "") {
|
||||
setSuggestions([]);
|
||||
setInlineSuggestion(prev => ({ ...prev, visible: false }));
|
||||
@ -273,6 +325,9 @@ const Search: React.FC<SearchProps> = ({
|
||||
|
||||
const searchResult = JSON.parse(result) as SearchResult;
|
||||
|
||||
// 检查组件是否仍然挂载
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
// 确保有suggestions字段且是数组
|
||||
if (!searchResult?.suggestions || !Array.isArray(searchResult.suggestions) || searchResult.suggestions.length === 0) {
|
||||
setSuggestions([]);
|
||||
@ -305,6 +360,9 @@ const Search: React.FC<SearchProps> = ({
|
||||
setInlineSuggestion(prev => ({ ...prev, visible: false }));
|
||||
}
|
||||
} catch (err) {
|
||||
// 检查组件是否仍然挂载
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
console.error("获取内联建议失败:", err);
|
||||
setInlineSuggestion(prev => ({ ...prev, visible: false }));
|
||||
setSelectedSuggestionIndex(0); // 重置选中索引
|
||||
@ -524,6 +582,14 @@ const Search: React.FC<SearchProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
// 取消之前的请求(虽然这是WASM调用,不是真正的网络请求,但保持一致性)
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
}
|
||||
|
||||
// 创建新的 AbortController
|
||||
abortControllerRef.current = new AbortController();
|
||||
|
||||
try {
|
||||
const page = isLoadMore ? currentPage : 1;
|
||||
|
||||
@ -544,6 +610,9 @@ const Search: React.FC<SearchProps> = ({
|
||||
|
||||
const resultJson = wasmModule.search_articles(indexData, JSON.stringify(req));
|
||||
|
||||
// 检查组件是否仍然挂载
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
if (!resultJson || resultJson.trim() === "") {
|
||||
console.error("返回的搜索结果为空");
|
||||
setLoadingState({
|
||||
@ -555,6 +624,9 @@ const Search: React.FC<SearchProps> = ({
|
||||
|
||||
const result = JSON.parse(resultJson) as SearchResult;
|
||||
|
||||
// 检查组件是否仍然挂载
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
// 预处理搜索结果
|
||||
for (const item of result.items) {
|
||||
if (item.heading_tree) {
|
||||
@ -584,6 +656,9 @@ const Search: React.FC<SearchProps> = ({
|
||||
// 更新加载状态
|
||||
setLoadingState(prev => ({ ...prev, status: 'success' }));
|
||||
} catch (err) {
|
||||
// 检查组件是否仍然挂载
|
||||
if (!isMountedRef.current) return;
|
||||
|
||||
console.error("搜索执行失败:", err);
|
||||
setLoadingState({
|
||||
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 = (
|
||||
<div className="relative [&_mark]:bg-yellow-200 dark:[&_mark]:bg-yellow-800">
|
||||
|
@ -19,6 +19,22 @@ import {
|
||||
} 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([
|
||||
import("three/examples/jsm/controls/OrbitControls.js"),
|
||||
@ -129,17 +145,32 @@ const WorldHeatmap: React.FC<WorldHeatmapProps> = ({ visitedPlaces }) => {
|
||||
|
||||
// 从公共目录加载地图数据
|
||||
useEffect(() => {
|
||||
// 创建 AbortController 用于在组件卸载时取消请求
|
||||
const controller = new AbortController();
|
||||
const signal = controller.signal;
|
||||
|
||||
const loadMapData = async () => {
|
||||
try {
|
||||
setMapLoading(true);
|
||||
setMapError(null);
|
||||
|
||||
// 从公共目录加载地图数据
|
||||
// 添加请求超时处理
|
||||
const timeout = setTimeout(() => controller.abort(), 30000); // 30秒超时
|
||||
|
||||
// 从公共目录加载地图数据,并使用 signal 和缓存控制
|
||||
const [worldDataResponse, chinaDataResponse] = await Promise.all([
|
||||
fetch('/maps/world.zh.json'),
|
||||
fetch('/maps/china.json')
|
||||
fetch('/maps/world.zh.json', {
|
||||
signal,
|
||||
headers: { 'Cache-Control': 'no-cache' }
|
||||
}),
|
||||
fetch('/maps/china.json', {
|
||||
signal,
|
||||
headers: { 'Cache-Control': 'no-cache' }
|
||||
})
|
||||
]);
|
||||
|
||||
clearTimeout(timeout);
|
||||
|
||||
if (!worldDataResponse.ok || !chinaDataResponse.ok) {
|
||||
throw new Error('无法获取地图数据');
|
||||
}
|
||||
@ -153,15 +184,23 @@ const WorldHeatmap: React.FC<WorldHeatmapProps> = ({ visitedPlaces }) => {
|
||||
});
|
||||
|
||||
setMapLoading(false);
|
||||
} catch (err) {
|
||||
console.error("加载地图数据失败:", err);
|
||||
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||
setMapError(`地图数据加载失败: ${errorMessage}`);
|
||||
} catch (err: any) {
|
||||
// 只有当请求不是被我们自己中断时才设置错误状态
|
||||
if (err.name !== 'AbortError') {
|
||||
console.error("加载地图数据失败:", err);
|
||||
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||
setMapError(`地图数据加载失败: ${errorMessage}`);
|
||||
}
|
||||
setMapLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadMapData();
|
||||
|
||||
// 清理函数:组件卸载时中断请求
|
||||
return () => {
|
||||
controller.abort();
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 加载WASM模块
|
||||
@ -177,7 +216,7 @@ const WorldHeatmap: React.FC<WorldHeatmapProps> = ({ visitedPlaces }) => {
|
||||
}
|
||||
setWasmModule(wasm as unknown as GeoWasmModule);
|
||||
setWasmError(null);
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
console.error("加载WASM模块失败:", err);
|
||||
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||
setWasmError(`WASM模块初始化失败: ${errorMessage}`);
|
||||
@ -203,7 +242,7 @@ const WorldHeatmap: React.FC<WorldHeatmapProps> = ({ visitedPlaces }) => {
|
||||
|
||||
setGeoProcessor(geoProcessorInstance);
|
||||
setWasmReady(true);
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error("WASM数据处理失败:", error);
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
setWasmError(`WASM数据处理失败: ${errorMessage}`);
|
||||
@ -689,19 +728,50 @@ const WorldHeatmap: React.FC<WorldHeatmapProps> = ({ visitedPlaces }) => {
|
||||
return null;
|
||||
};
|
||||
|
||||
// 简化的动画循环函数
|
||||
// 优化的动画循环函数
|
||||
const animate = () => {
|
||||
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();
|
||||
|
||||
|
||||
// 渲染
|
||||
renderer.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);
|
||||
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error("Three.js初始化失败:", error);
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
setMapError(`3D地图初始化失败: ${errorMessage}`);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -26,19 +26,47 @@ function delay(ms: number) {
|
||||
|
||||
// 带超时的 fetch 函数
|
||||
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(() => {
|
||||
controller.abort();
|
||||
timeoutController.abort();
|
||||
}, timeoutMs);
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
signal
|
||||
});
|
||||
return response;
|
||||
// 使用已有的信号和我们的超时信号
|
||||
if (existingSignal) {
|
||||
// 如果已经取消了,直接抛出异常
|
||||
if (existingSignal.aborted) {
|
||||
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 {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
@ -314,6 +342,24 @@ export const GET: APIRoute = async ({ request }) => {
|
||||
});
|
||||
} catch (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));
|
||||
|
||||
if (retries < MAX_RETRIES) {
|
||||
@ -332,6 +378,21 @@ export const GET: APIRoute = async ({ request }) => {
|
||||
// 检查是否是常见错误类型并返回对应错误信息
|
||||
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('频繁')) {
|
||||
return new Response(JSON.stringify({
|
||||
@ -387,6 +448,22 @@ export const GET: APIRoute = async ({ request }) => {
|
||||
});
|
||||
} catch (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({
|
||||
error: '获取豆瓣数据失败',
|
||||
message: error instanceof Error ? error.message : '未知错误'
|
||||
|
@ -87,9 +87,26 @@ export async function GET({ request }: APIContext) {
|
||||
headers
|
||||
});
|
||||
} 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({
|
||||
error: '处理请求错误',
|
||||
message: error instanceof Error ? error.message : '未知错误'
|
||||
message: error instanceof Error ? error.message : '未知错误',
|
||||
type: 'server'
|
||||
}), {
|
||||
status: 500,
|
||||
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) {
|
||||
const maxRetries = 3;
|
||||
let retryCount = 0;
|
||||
@ -193,6 +316,11 @@ async function fetchGithubProjects(username: string, organization: string, page:
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
// 检查是否为中止错误
|
||||
if (error instanceof Error && (error.name === 'AbortError' || error.message.includes('aborted'))) {
|
||||
throw error; // 中止错误直接抛出,不重试
|
||||
}
|
||||
|
||||
retryCount++;
|
||||
|
||||
if (retryCount >= maxRetries) {
|
||||
@ -218,6 +346,7 @@ async function fetchGiteaProjects(username: string, organization: string, page:
|
||||
try {
|
||||
const perPage = config.perPage || 10;
|
||||
const giteaUrl = config.url;
|
||||
const signal = config.signal; // 获取可能的 AbortSignal
|
||||
|
||||
if (!giteaUrl) {
|
||||
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}`;
|
||||
}
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
const response = await fetchWithRetry(apiUrl, {
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
...(config.token ? { 'Authorization': `token ${config.token}` } : {})
|
||||
}
|
||||
});
|
||||
},
|
||||
signal // 传递 AbortSignal
|
||||
}, 3, 15000); // 最多重试3次,每次超时15秒
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Gitea API 请求失败: ${response.statusText}`);
|
||||
@ -273,6 +403,13 @@ async function fetchGiteaProjects(username: string, organization: string, page:
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
// 检查是否为中止错误,将其向上传播
|
||||
if (error instanceof Error && (error.name === 'AbortError' || error.message.includes('aborted'))) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
console.error('获取 Gitea 项目失败:', error);
|
||||
|
||||
return {
|
||||
projects: [],
|
||||
pagination: {
|
||||
@ -288,6 +425,7 @@ async function fetchGiteaProjects(username: string, organization: string, page:
|
||||
async function fetchGiteeProjects(username: string, organization: string, page: number, config: any) {
|
||||
try {
|
||||
const perPage = config.perPage || 10;
|
||||
const signal = config.signal; // 获取可能的 AbortSignal
|
||||
|
||||
const giteeUsername = username || config.username;
|
||||
|
||||
@ -306,7 +444,9 @@ async function fetchGiteeProjects(username: string, organization: string, page:
|
||||
apiUrl += `&access_token=${config.token}`;
|
||||
}
|
||||
|
||||
const response = await fetch(apiUrl);
|
||||
const response = await fetchWithRetry(apiUrl, {
|
||||
signal // 传递 AbortSignal
|
||||
}, 3, 15000); // 最多重试3次,每次超时15秒
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Gitee API 请求失败: ${response.statusText}`);
|
||||
@ -340,6 +480,13 @@ async function fetchGiteeProjects(username: string, organization: string, page:
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
// 检查是否为中止错误,将其向上传播
|
||||
if (error instanceof Error && (error.name === 'AbortError' || error.message.includes('aborted'))) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
console.error('获取 Gitee 项目失败:', error);
|
||||
|
||||
return {
|
||||
projects: [],
|
||||
pagination: {
|
||||
|
Loading…
Reference in New Issue
Block a user