前端-请求接口中断处理指南
2026年1月19日 16:32
请求中断处理指南
📋 今日处理总结
处理的问题
-
实现
eligibleForPrizeExchange请求中断:当打开 auto select 时,中断正在进行的eligibleForPrizeExchange请求 -
实现
target-prizes请求中断:当关闭 auto select 时,中断正在进行的target-prizes请求 - 解决竞态条件问题:防止旧请求的结果覆盖新请求的结果
- 修复 loading 状态管理:确保 loading 状态能正确显示和更新
核心实现逻辑
1. 使用 AbortController + RequestId 双重保障
// 1. 定义状态管理
const abortController = ref<AbortController | null>(null);
const currentRequestId = ref<number>(0);
// 2. 在请求函数中
const fetchData = async () => {
// 中断之前的请求
if (abortController.value) {
abortController.value.abort();
}
// 创建新的 AbortController 和 requestId
const requestId = ++currentRequestId.value;
const newAbortController = new AbortController();
abortController.value = newAbortController;
try {
// 检查 requestId 是否仍然是最新的
if (requestId !== currentRequestId.value) {
return; // 已被新请求取代,直接返回
}
const signal = newAbortController.signal;
// 发送请求,传递 signal
const result = await apiCall(params, signal);
// 请求完成后,再次检查 requestId
if (requestId !== currentRequestId.value) {
return; // 已被新请求取代,忽略结果
}
// 检查当前状态是否仍然匹配
if (!shouldProcessResult()) {
return; // 状态已改变,不处理结果
}
// 处理结果
updateData(result);
} catch (error) {
// 如果是 AbortError,不需要处理错误
if (error instanceof Error && error.name !== 'AbortError') {
handleError(error);
}
} finally {
// 只有当前请求完成时,才清理 AbortController
if (requestId === currentRequestId.value) {
abortController.value = null;
}
}
};
2. 在 Store 中传递 signal
// stores/example.ts
async function fetchData(params: any, signal?: AbortSignal) {
const uniqueKey = `fetchData-${Date.now()}-${Math.random()}`;
const { data, execute } = useApiFetch(url, {
method: "POST",
body: JSON.stringify(params),
signal: signal,
key: uniqueKey, // 确保每次创建新实例
});
await execute();
// 处理结果...
}
// 在 catch 中处理 AbortError
catch (e: any) {
if (e?.name === 'AbortError' || e?.message?.includes('aborted')) {
return { success: false, data: null, message: 'Request aborted' };
}
// 处理其他错误...
}
3. 在 useApiFetch 中动态设置 signal
// composables/useApiFetch.ts
onRequest({ options }) {
if (signal !== undefined && signal !== null) {
options.signal = signal;
// 检查 signal 是否已经被中断
if (signal.aborted) {
throw new DOMException('The operation was aborted.', 'AbortError');
}
}
}
🎯 通用处理模式
模式一:单一请求中断
场景:用户快速操作,需要中断之前的请求
const abortController = ref<AbortController | null>(null);
const currentRequestId = ref<number>(0);
const fetchData = async () => {
// 1. 中断之前的请求
if (abortController.value) {
abortController.value.abort();
}
// 2. 创建新的请求标识
const requestId = ++currentRequestId.value;
const controller = new AbortController();
abortController.value = controller;
try {
// 3. 检查是否仍然是最新请求
if (requestId !== currentRequestId.value) return;
// 4. 发送请求
const result = await apiCall(controller.signal);
// 5. 再次检查
if (requestId !== currentRequestId.value) return;
// 6. 处理结果
processResult(result);
} catch (error) {
if (error.name !== 'AbortError') {
handleError(error);
}
} finally {
if (requestId === currentRequestId.value) {
abortController.value = null;
}
}
};
模式二:多请求互斥中断
场景:两个不同的请求,一个触发时中断另一个
const requestAController = ref<AbortController | null>(null);
const requestBController = ref<AbortController | null>(null);
const fetchA = async () => {
// 中断 B
if (requestBController.value) {
requestBController.value.abort();
requestBController.value = null;
}
// 开始 A
const controller = new AbortController();
requestAController.value = controller;
// ... 发送请求
};
const fetchB = async () => {
// 中断 A
if (requestAController.value) {
requestAController.value.abort();
requestAController.value = null;
}
// 开始 B
const controller = new AbortController();
requestBController.value = controller;
// ... 发送请求
};
模式三:状态检查 + RequestId 双重验证
场景:请求结果需要匹配当前状态(如 autoSelect 状态)
const fetchData = async () => {
const requestId = ++currentRequestId.value;
const controller = new AbortController();
try {
const result = await apiCall(controller.signal);
// 1. 检查 requestId(防止竞态条件)
if (requestId !== currentRequestId.value) return;
// 2. 检查状态(确保结果匹配当前状态)
if (!isValidState()) return;
// 3. 处理结果
updateData(result);
} catch (error) {
// 处理错误...
}
};
🔍 常见场景及处理方法
场景 1:搜索框输入
问题:用户快速输入,需要中断之前的搜索请求
解决方法:
const searchController = ref<AbortController | null>(null);
const searchRequestId = ref<number>(0);
const handleSearch = debounce(async (keyword: string) => {
// 中断之前的搜索
if (searchController.value) {
searchController.value.abort();
}
const requestId = ++searchRequestId.value;
const controller = new AbortController();
searchController.value = controller;
try {
if (requestId !== searchRequestId.value) return;
const results = await searchApi(keyword, controller.signal);
if (requestId !== searchRequestId.value) return;
updateSearchResults(results);
} catch (error) {
if (error.name !== 'AbortError') {
handleError(error);
}
}
}, 300);
场景 2:标签页切换
问题:切换标签页时,需要中断当前标签页的请求
解决方法:
const tabControllers = ref<Map<string, AbortController>>(new Map());
const fetchTabData = async (tabId: string) => {
// 中断当前标签页的请求
const currentController = tabControllers.value.get(tabId);
if (currentController) {
currentController.abort();
}
// 创建新的请求
const controller = new AbortController();
tabControllers.value.set(tabId, controller);
try {
const data = await fetchTabDataApi(tabId, controller.signal);
updateTabData(tabId, data);
} catch (error) {
if (error.name !== 'AbortError') {
handleError(error);
}
}
};
场景 3:表单提交
问题:用户快速点击提交按钮,需要防止重复提交
解决方法:
const submitController = ref<AbortController | null>(null);
const isSubmitting = ref<boolean>(false);
const handleSubmit = async () => {
if (isSubmitting.value) {
// 中断之前的提交
if (submitController.value) {
submitController.value.abort();
}
}
isSubmitting.value = true;
const controller = new AbortController();
submitController.value = controller;
try {
await submitForm(formData, controller.signal);
showSuccess();
} catch (error) {
if (error.name !== 'AbortError') {
showError(error);
}
} finally {
isSubmitting.value = false;
submitController.value = null;
}
};
场景 4:下拉刷新
问题:用户快速下拉刷新,需要中断之前的刷新请求
解决方法:
const refreshController = ref<AbortController | null>(null);
const handleRefresh = async () => {
// 中断之前的刷新
if (refreshController.value) {
refreshController.value.abort();
}
const controller = new AbortController();
refreshController.value = controller;
try {
const data = await refreshData(controller.signal);
updateData(data);
} catch (error) {
if (error.name !== 'AbortError') {
handleError(error);
}
} finally {
refreshController.value = null;
}
};
✅ 最佳实践
1. 必须使用 RequestId
原因:防止竞态条件,确保只有最新请求的结果被使用
// ✅ 正确
const requestId = ++currentRequestId.value;
// ... 请求完成后检查
if (requestId !== currentRequestId.value) return;
// ❌ 错误:没有 requestId,无法防止竞态条件
const controller = new AbortController();
2. 多次检查 RequestId
原因:异步操作中,状态可能随时改变
// ✅ 正确:在关键步骤前都检查
const result = await apiCall();
if (requestId !== currentRequestId.value) return;
processData(result);
if (requestId !== currentRequestId.value) return;
updateUI(result);
3. 状态检查 + RequestId 双重验证
原因:确保结果不仅是最新的,还要匹配当前状态
// ✅ 正确
if (requestId !== currentRequestId.value) return;
if (!isValidState()) return; // 检查状态是否匹配
// ❌ 错误:只检查 requestId,不检查状态
if (requestId !== currentRequestId.value) return;
// 直接使用结果,可能状态已改变
4. 正确处理 AbortError
原因:AbortError 是正常的取消操作,不应该显示错误
// ✅ 正确
catch (error) {
if (error.name !== 'AbortError') {
handleError(error); // 只处理真正的错误
}
}
// ❌ 错误:所有错误都处理,包括 AbortError
catch (error) {
handleError(error); // 会显示"请求被中断"的错误提示
}
5. 在 finally 中清理 AbortController
原因:确保只有当前请求完成时才清理,避免影响新请求
// ✅ 正确
finally {
if (requestId === currentRequestId.value) {
abortController.value = null; // 只有当前请求完成时才清理
}
}
// ❌ 错误:总是清理,可能影响新请求
finally {
abortController.value = null; // 可能清理了新请求的 controller
}
6. 使用唯一的 key 创建新的 useApiFetch 实例
原因:避免 useFetch 缓存导致的问题
// ✅ 正确
const uniqueKey = `apiCall-${Date.now()}-${Math.random()}`;
const { data, execute } = useApiFetch(url, {
signal: signal,
key: uniqueKey, // 确保每次创建新实例
});
// ❌ 错误:使用相同的 key,可能导致缓存问题
const { data, execute } = useApiFetch(url, {
signal: signal,
// 没有 key,可能使用缓存的实例
});
📝 代码模板
完整模板
// 1. 定义状态
const abortController = ref<AbortController | null>(null);
const currentRequestId = ref<number>(0);
const isLoading = ref<boolean>(false);
// 2. 请求函数
const fetchData = async (params: any) => {
// 中断之前的请求
if (abortController.value) {
abortController.value.abort();
}
// 创建新的请求标识
const requestId = ++currentRequestId.value;
const controller = new AbortController();
abortController.value = controller;
isLoading.value = true;
try {
// 检查 1:请求开始前
if (requestId !== currentRequestId.value) {
isLoading.value = false;
return;
}
const signal = controller.signal;
if (!signal) {
isLoading.value = false;
return;
}
// 发送请求
const result = await store.fetchData(params, signal);
// 检查 2:请求完成后
if (requestId !== currentRequestId.value) {
return;
}
// 检查 3:状态验证(如果需要)
if (!isValidState()) {
return;
}
// 检查 4:更新数据前
if (requestId !== currentRequestId.value) {
return;
}
// 处理结果
updateData(result);
} catch (error: any) {
// 处理错误(忽略 AbortError)
if (error instanceof Error && error.name !== 'AbortError') {
handleError(error);
}
} finally {
// 清理(只有当前请求完成时)
if (requestId === currentRequestId.value) {
abortController.value = null;
}
isLoading.value = false;
}
};
// 3. 中断函数(在状态改变时调用)
const interruptRequest = () => {
if (abortController.value) {
abortController.value.abort();
abortController.value = null;
currentRequestId.value++; // 更新 requestId,确保旧请求结果被忽略
}
};
🚨 注意事项
- 不要忘记传递 signal:确保 signal 正确传递到 API 调用
- 不要忘记检查 requestId:防止竞态条件
- 不要忘记检查状态:确保结果匹配当前状态
- 不要忘记处理 AbortError:避免显示"请求被中断"的错误
- 不要忘记清理 AbortController:避免内存泄漏
- 使用唯一的 key:避免 useFetch 缓存问题
📚 相关文件
-
pages/wallet/components/redemption/WalletSelectionSection.vue- target-prizes 中断实现 -
pages/wallet/components/RedeemWithPrizesModal.vue- eligibleForPrizeExchange 中断实现 -
stores/wallet.ts- API 调用中的 signal 传递 -
composables/useApiFetch.ts- signal 的动态设置