适用于Vue项目中的API进度实时追踪:
usePolling
import { ref, onMounted, onUnmounted, Ref, watch } from 'vue';
type UsePollingOptions<T> = {
// 要轮询执行的异步函数
asyncFunction: () => Promise<T>;
// 轮询间隔时间(毫秒)
interval: number;
// 是否在初始化时立即执行一次
immediate?: boolean;
// 请求成功回调
onSuccess?: (data: T) => void;
// 请求失败回调
onError?: (error: Error) => void;
// 是否自动停止轮询的条件
stopWhen?: (data: T) => boolean;
};
type UsePollingResult<T> = {
// 最新请求数据
data: Ref<T | null>;
// 错误信息
error: Ref<Error | null>;
// 是否正在加载中
isLoading: Ref<boolean>;
// 是否正在轮询中
isPolling: Ref<boolean>;
// 开始轮询
startPolling: () => void;
// 停止轮询
stopPolling: () => void;
// 手动触发一次请求
refresh: () => Promise<void>;
};
/**
* 异步轮询组合式函数,用于API进度实时追踪
* @param options 轮询配置选项
* @returns 轮询状态和控制函数
*/
export function usePolling<T>({
asyncFunction,
interval,
immediate = true,
onSuccess,
onError,
stopWhen
}: UsePollingOptions<T>): UsePollingResult<T> {
// 状态管理
const data = ref<T | null>(null) as Ref<T | null>;
const error = ref<Error | null>(null);
const isLoading = ref<boolean>(false);
const isPolling = ref<boolean>(false);
// 定时器和取消控制器引用
let timer: NodeJS.Timeout | null = null;
let abortController: AbortController | null = null;
// 执行异步请求
const execute = async () => {
// 如果已有请求,取消它
if (abortController) {
abortController.abort();
}
abortController = new AbortController();
isLoading.value = true;
error.value = null;
try {
const result = await asyncFunction();
data.value = result;
onSuccess?.(result);
// 检查是否需要停止轮询
if (stopWhen && stopWhen(result)) {
stopPolling();
}
} catch (err) {
// 忽略取消错误
if (err instanceof Error && err.name !== 'AbortError') {
error.value = err;
onError?.(err);
}
} finally {
isLoading.value = false;
}
};
// 开始轮询
const startPolling = () => {
if (isPolling.value) return;
isPolling.value = true;
// 立即执行一次还是等待第一个间隔
if (immediate) {
execute().then(() => {
if (isPolling.value) {
timer = setInterval(execute, interval);
}
});
} else {
timer = setInterval(execute, interval);
}
};
// 停止轮询
const stopPolling = () => {
if (timer) {
clearInterval(timer);
timer = null;
}
if (abortController) {
abortController.abort();
abortController = null;
}
isPolling.value = false;
};
// 手动触发一次请求
const refresh = async () => {
// 如果正在轮询,先停止再重新开始
const wasPolling = isPolling.value;
if (wasPolling) {
stopPolling();
}
await execute();
// 如果之前在轮询,执行完后继续轮询
if (wasPolling) {
timer = setInterval(execute, interval);
isPolling.value = true;
}
};
// 组件挂载时初始化轮询
onMounted(() => {
if (immediate) {
startPolling();
}
});
// 组件卸载时清理
onUnmounted(() => {
stopPolling();
});
return {
data,
error,
isLoading,
isPolling,
startPolling,
stopPolling,
refresh
};
}
Vue组件中使用示例
<template>
<div class="progress-tracker">
<div v-if="isLoading">加载中...</div>
<div v-else-if="error" class="error">{{ error.message }}</div>
<div v-else-if="data">
<p>当前进度: {{ data.progress }}%</p>
<button @click="startPolling" :disabled="isPolling">开始轮询</button>
<button @click="stopPolling" :disabled="!isPolling">停止轮询</button>
<button @click="refresh">手动刷新</button>
</div>
</div>
</template>
<script setup lang="ts">
import { usePolling } from '@/composables/usePolling';
// 模拟API请求函数
const fetchProgress = async () => {
const response = await fetch('/api/progress');
if (!response.ok) throw new Error('获取进度失败');
return response.json();
};
// 使用轮询钩子
const {
data,
error,
isLoading,
isPolling,
startPolling,
stopPolling,
refresh
} = usePolling({
asyncFunction: fetchProgress,
interval: 3000, // 3秒轮询一次
immediate: true,
// 当进度达到100%时自动停止轮询
stopWhen: (data) => data.progress >= 100,
onSuccess: (progress) => {
console.log('进度更新:', progress);
if (progress.progress >= 100) {
alert('任务已完成!');
}
},
onError: (error) => {
console.error('轮询错误:', error);
}
});
</script>
Hook 优势
- 自动清理机制:组件卸载时自动停止轮询并取消请求,避免内存泄漏
- 灵活的控制选项:支持
stopWhen
条件自动停止,refresh
手动刷新 - 错误处理优化:区分普通错误和取消错误,避免误报