cpms_operation_platform/src/hooks/useRequest.ts

171 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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