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,
|
||
};
|
||
}
|