/** * useHttp —— 全局 HTTP 工具(axios 实例 + 带重试请求 + 超时包装)。 * * 从原 App.vue 原样搬移,逻辑零改动,仅改为导出供各 composable 复用。 * 含缓存穿透拦截器(createCacheBusterInterceptor),与之前的审计加固一致。 */ import axios, { AxiosError } from "axios"; import { APM_STORE_BASE_URL } from "../global/storeConfig"; import { createCacheBusterInterceptor } from "../global/cacheBusterInterceptor"; // Axios 全局配置 export const axiosInstance = axios.create({ baseURL: APM_STORE_BASE_URL, timeout: 5000, // 增加到 5 秒,避免网络波动导致的超时 }); // C2:数据 JSON(applist / categories / sidebar-config 等)追加 ?_t 版本戳, // 穿透 CDN 边缘缓存,确保返回最新列表。复用共享缓存穿透拦截器(带 TTL 复用戳, // 避免同会话频繁击穿缓存)。与主进程 onBeforeSendHeaders 注入的 no-cache 互为兜底。 // 这是有意的缓存击穿策略(非缺陷),用于解决商店目录强缓存导致的更新延迟。 axiosInstance.interceptors.request.use(createCacheBusterInterceptor()); // 5xx / 网络错误 / 超时重试;4xx(如 404)快速失败 const RETRYABLE_STATUS = new Set([502, 503, 504]); // 计算重试延迟:优先尊重服务端 Retry-After 头;429 限流用指数退避(封顶 30s); // 其余 5xx 用固定延迟。避免对限流场景频繁重试浪费带宽。 const calculateRetryDelay = ( status: number | undefined, attempt: number, retryAfterHeader?: string, fixedDelayMs = 500, ): number => { if (retryAfterHeader) { const seconds = Number.parseInt(retryAfterHeader, 10); if (!Number.isNaN(seconds) && seconds > 0) return seconds * 1000; } if (status === 429) { return Math.min(1000 * 2 ** attempt, 30000); } return fixedDelayMs; }; export const fetchWithRetry = async ( path: string, signal?: AbortSignal, retries = 2, retryDelayMs = 500, ): Promise => { for (let attempt = 0; attempt <= retries; attempt++) { try { const resp = await axiosInstance.get(path, { signal }); return resp.data; } catch (err) { const ae = err as AxiosError; const status = ae.response?.status; // 4xx(含 404)快速失败,不重试 if (status && status < 500 && status !== 429) { return null; } // 仅对网络错误 / 5xx / 429 重试 const retryable = !status || RETRYABLE_STATUS.has(status) || status === 429; if (!retryable || attempt === retries) { return null; } const retryAfter = ae.response?.headers?.["retry-after"] as | string | undefined; const delay = calculateRetryDelay( status, attempt, retryAfter, retryDelayMs, ); await new Promise((r) => setTimeout(r, delay)); } } return null; }; // 全局请求取消控制器(目录加载期间可整体取消) export const rootAbortController = new AbortController(); // 给 Promise 包一层超时,超时即 reject。finally 中清理定时器,避免泄漏。 export const withTimeout = async ( promise: Promise, ms: number, label = "operation", ): Promise => { let timer: ReturnType | undefined; const timeout = new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(`${label} 超时 (${ms}ms)`)), ms); }); try { return await Promise.race([promise, timeout]); } finally { if (timer) clearTimeout(timer); } };