171 lines
4.3 KiB
TypeScript
171 lines
4.3 KiB
TypeScript
|
|
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
|||
|
|
import { ref, Ref, watch, WatchSource, onUnmounted, computed } from 'vue';
|
|||
|
|
|
|||
|
|
export interface UseRequestReturn<T> {
|
|||
|
|
/** 纯净的数据,不包含后端外层壳 */
|
|||
|
|
data: Ref<T | undefined>;
|
|||
|
|
/** 加载状态 */
|
|||
|
|
loading: Ref<boolean>;
|
|||
|
|
/** 错误对象 */
|
|||
|
|
error: Ref<Error | null>;
|
|||
|
|
/** 手动触发请求 */
|
|||
|
|
run: (...args: any[]) => Promise<T | undefined>;
|
|||
|
|
/** 取消请求 */
|
|||
|
|
cancel: () => void;
|
|||
|
|
/** 重置数据 */
|
|||
|
|
reset: () => void;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function useRequest<T>(
|
|||
|
|
fetcher: (...args: any[]) => Promise<any>,
|
|||
|
|
options: {
|
|||
|
|
/** 是否手动触发,默认 false (即自动触发) */
|
|||
|
|
manual?: boolean;
|
|||
|
|
/** 初始数据 */
|
|||
|
|
initialData?: T;
|
|||
|
|
/** 依赖数组,变化时重新请求 */
|
|||
|
|
refreshDeps?: WatchSource[];
|
|||
|
|
/**
|
|||
|
|
* 数据格式化函数
|
|||
|
|
* 默认逻辑:如果返回是对象且有 data 属性,则返回 res.data,否则返回原数据
|
|||
|
|
*/
|
|||
|
|
formatResult?: (res: any) => T;
|
|||
|
|
/** 是否准备好可以发起请求,默认 true */
|
|||
|
|
ready?: Ref<boolean>;
|
|||
|
|
} = {},
|
|||
|
|
): UseRequestReturn<T> & {
|
|||
|
|
/** 获取当前 AbortController 的 signal,可传递给 fetcher */
|
|||
|
|
getSignal: () => AbortSignal | undefined;
|
|||
|
|
} {
|
|||
|
|
const {
|
|||
|
|
manual = false,
|
|||
|
|
initialData,
|
|||
|
|
refreshDeps,
|
|||
|
|
formatResult = (res) => (res && typeof res === 'object' && 'data' in res ? res.data : res),
|
|||
|
|
ready = ref(true),
|
|||
|
|
} = options;
|
|||
|
|
|
|||
|
|
const data = ref<T | undefined>(initialData);
|
|||
|
|
const loading = ref(false);
|
|||
|
|
const error = ref<Error | null>(null);
|
|||
|
|
|
|||
|
|
let abortController: AbortController | null = null;
|
|||
|
|
let isUnmounted = false;
|
|||
|
|
|
|||
|
|
onUnmounted(() => {
|
|||
|
|
isUnmounted = true;
|
|||
|
|
if (abortController) {
|
|||
|
|
abortController.abort();
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
const run = async (...args: any[]): Promise<T | undefined> => {
|
|||
|
|
if (isUnmounted) return undefined;
|
|||
|
|
|
|||
|
|
// 取消之前的请求
|
|||
|
|
if (abortController) {
|
|||
|
|
abortController.abort();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 创建新的 AbortController
|
|||
|
|
abortController = new AbortController();
|
|||
|
|
const currentSignal = abortController.signal;
|
|||
|
|
|
|||
|
|
loading.value = true;
|
|||
|
|
error.value = null;
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
// 将 signal 作为最后一个参数传递给 fetcher
|
|||
|
|
// 调用方可以通过最后一个参数获取 signal: async (...args, { signal }) => {...}
|
|||
|
|
const res = await fetcher(...args, { signal: currentSignal });
|
|||
|
|
|
|||
|
|
if (currentSignal.aborted || isUnmounted) {
|
|||
|
|
return undefined;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const formattedData = formatResult(res);
|
|||
|
|
|
|||
|
|
if (!isUnmounted) {
|
|||
|
|
data.value = formattedData;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return formattedData;
|
|||
|
|
} catch (e: any) {
|
|||
|
|
if (e.name === 'AbortError' || e.message?.includes('aborted')) {
|
|||
|
|
return undefined;
|
|||
|
|
}
|
|||
|
|
if (!isUnmounted) {
|
|||
|
|
error.value = e;
|
|||
|
|
}
|
|||
|
|
return undefined;
|
|||
|
|
} finally {
|
|||
|
|
// 只有当前请求未被取消时才重置 loading
|
|||
|
|
if (!currentSignal.aborted && !isUnmounted) {
|
|||
|
|
loading.value = false;
|
|||
|
|
}
|
|||
|
|
// 如果这是当前活动的 controller,清理引用
|
|||
|
|
if (abortController?.signal === currentSignal) {
|
|||
|
|
abortController = null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const cancel = () => {
|
|||
|
|
if (abortController) {
|
|||
|
|
abortController.abort();
|
|||
|
|
loading.value = false;
|
|||
|
|
abortController = null;
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const reset = () => {
|
|||
|
|
cancel();
|
|||
|
|
data.value = initialData;
|
|||
|
|
error.value = null;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// 使用 shouldAutoRun 统一管理自动请求,避免 refreshDeps 和 immediate watch 重复触发 run
|
|||
|
|
const shouldAutoRun = computed(() => !manual && ready.value);
|
|||
|
|
let hasAutoRun = false; // 标记是否已自动执行过,防止重复触发
|
|||
|
|
|
|||
|
|
watch(
|
|||
|
|
shouldAutoRun,
|
|||
|
|
(val) => {
|
|||
|
|
if (val && !hasAutoRun) {
|
|||
|
|
hasAutoRun = true;
|
|||
|
|
run();
|
|||
|
|
} else if (!val) {
|
|||
|
|
// 当条件不满足时重置 hasAutoRun,使下次满足条件时能再次自动执行
|
|||
|
|
hasAutoRun = false;
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
{ immediate: true },
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// refreshDeps 变化时重新请求(初始化时不再重复触发)
|
|||
|
|
if (refreshDeps && refreshDeps.length > 0) {
|
|||
|
|
watch(
|
|||
|
|
refreshDeps,
|
|||
|
|
() => {
|
|||
|
|
if (shouldAutoRun.value) {
|
|||
|
|
run();
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
{ deep: true },
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const getSignal = () => abortController?.signal;
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
// @ts-ignore
|
|||
|
|
data,
|
|||
|
|
loading,
|
|||
|
|
error,
|
|||
|
|
run,
|
|||
|
|
cancel,
|
|||
|
|
reset,
|
|||
|
|
getSignal,
|
|||
|
|
};
|
|||
|
|
}
|