增加请求错误处理
This commit is contained in:
parent
d3e0eddff7
commit
1336dc9081
@ -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,6 +738,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 :
|
||||||
@ -732,11 +752,17 @@ 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 {
|
||||||
|
// 检查组件是否仍然挂载
|
||||||
|
if (isMountedRef.current) {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 加载WASM模块
|
// 加载WASM模块
|
||||||
@ -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,29 +826,57 @@ 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 {
|
||||||
|
// 检查组件是否仍然挂载
|
||||||
|
if (isMountedRef.current) {
|
||||||
setIsLoading(false);
|
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 {
|
||||||
|
// 检查组件是否仍然挂载
|
||||||
|
if (isMountedRef.current) {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadIndexData();
|
loadIndexData();
|
||||||
|
|
||||||
|
// 组件卸载时清理
|
||||||
|
return () => {
|
||||||
|
if (abortControllerRef.current) {
|
||||||
|
abortControllerRef.current.abort();
|
||||||
|
}
|
||||||
|
};
|
||||||
}, [wasmModule]);
|
}, [wasmModule]);
|
||||||
|
|
||||||
// 检查activeFilters变化
|
// 检查activeFilters变化
|
||||||
|
@ -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,11 +165,11 @@ 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]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -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}
|
||||||
|
@ -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,10 +170,13 @@ const MediaGrid: React.FC<MediaGridProps> = ({ type, doubanId }) => {
|
|||||||
setItems([]);
|
setItems([]);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
// 检查组件是否已卸载
|
||||||
|
if (isMountedRef.current) {
|
||||||
// 重置加载状态
|
// 重置加载状态
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
stateRef.current.isLoading = 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);
|
||||||
|
|
||||||
|
@ -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">
|
||||||
|
@ -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) {
|
||||||
|
// 只有当请求不是被我们自己中断时才设置错误状态
|
||||||
|
if (err.name !== 'AbortError') {
|
||||||
console.error("加载地图数据失败:", err);
|
console.error("加载地图数据失败:", err);
|
||||||
const errorMessage = err instanceof Error ? err.message : String(err);
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||||
setMapError(`地图数据加载失败: ${errorMessage}`);
|
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}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
// 使用已有的信号和我们的超时信号
|
||||||
|
if (existingSignal) {
|
||||||
|
// 如果已经取消了,直接抛出异常
|
||||||
|
if (existingSignal.aborted) {
|
||||||
|
throw new DOMException('已被用户取消', 'AbortError');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建一个监听器,当外部信号中止时,也中止我们的控制器
|
||||||
|
const abortListener = () => timeoutController.abort();
|
||||||
|
existingSignal.addEventListener('abort', abortListener);
|
||||||
|
|
||||||
|
// 进行请求,但只使用我们的超时信号
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
...options,
|
...options,
|
||||||
signal
|
signal: timeoutSignal
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 移除监听器
|
||||||
|
existingSignal.removeEventListener('abort', abortListener);
|
||||||
|
|
||||||
return response;
|
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 : '未知错误'
|
||||||
|
@ -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: {
|
||||||
|
Loading…
Reference in New Issue
Block a user