115 lines
3.6 KiB
TypeScript
115 lines
3.6 KiB
TypeScript
import { onMounted, onScopeDispose, ref, watch, type Ref } from 'vue'
|
||
import { isCancel } from 'axios'
|
||
|
||
/** fetcher:业务参数在前,AbortSignal 固定作为最后一个参数传入(用于取消请求)。 */
|
||
export type RequestFetcher<TData, TParams extends unknown[]> = (...args: [...TParams, AbortSignal]) => Promise<TData>
|
||
|
||
export interface UseRequestOptions<TData, TParams extends unknown[]> {
|
||
/** true 时挂载不自动请求,需手动 run。 */
|
||
manual?: boolean
|
||
/** 首次自动请求 / run() 无参时使用的参数。 */
|
||
defaultParams?: TParams
|
||
/** 响应式依赖,变化时自动用上次参数重新请求。 */
|
||
refreshDeps?: unknown[]
|
||
onBefore?: (params: TParams) => void
|
||
onSuccess?: (data: TData, params: TParams) => void
|
||
onError?: (error: unknown, params: TParams) => void
|
||
onFinally?: (params: TParams) => void
|
||
}
|
||
|
||
export interface UseRequestResult<TData, TParams extends unknown[]> {
|
||
data: Ref<TData | undefined>
|
||
loading: Ref<boolean>
|
||
error: Ref<unknown>
|
||
/** 手动触发(成功返回 data,失败/取消返回 undefined)。 */
|
||
run: (...args: TParams) => Promise<TData | undefined>
|
||
/** 用上一次的参数重新请求。 */
|
||
refresh: () => Promise<TData | undefined>
|
||
/** 取消当前请求。 */
|
||
cancel: () => void
|
||
/** 本地修改 data(不回发请求)。 */
|
||
mutate: (data: TData | ((prev: TData | undefined) => TData)) => void
|
||
}
|
||
|
||
/**
|
||
* 数据请求 Hook(类似 ahooks 的 useRequest):
|
||
* 管理 data/loading/error 状态,支持手动触发、依赖刷新、取消与本地修改。
|
||
*/
|
||
export function useRequest<TData, TParams extends unknown[]>(
|
||
fetcher: RequestFetcher<TData, TParams>,
|
||
options: UseRequestOptions<TData, TParams> = {},
|
||
): UseRequestResult<TData, TParams> {
|
||
const { manual = false, defaultParams = [] as unknown as TParams, refreshDeps } = options
|
||
|
||
const data = ref<TData>()
|
||
const loading = ref(false)
|
||
const error = ref<unknown>()
|
||
|
||
let lastParams: TParams = defaultParams
|
||
let controller: AbortController | null = null
|
||
let seq = 0
|
||
|
||
async function run(...args: TParams): Promise<TData | undefined> {
|
||
controller?.abort()
|
||
controller = new AbortController()
|
||
const mySeq = ++seq
|
||
lastParams = args.length > 0 ? args : lastParams
|
||
|
||
loading.value = true
|
||
error.value = undefined
|
||
options.onBefore?.(lastParams)
|
||
|
||
try {
|
||
const result = await fetcher(...lastParams, controller.signal)
|
||
if (mySeq !== seq) return undefined // 已被更新的请求取代
|
||
data.value = result
|
||
options.onSuccess?.(result, lastParams)
|
||
return result
|
||
} catch (e) {
|
||
if (isCanceled(e)) return undefined
|
||
if (mySeq !== seq) return undefined
|
||
error.value = e
|
||
options.onError?.(e, lastParams)
|
||
return undefined
|
||
} finally {
|
||
if (mySeq === seq) loading.value = false
|
||
options.onFinally?.(lastParams)
|
||
}
|
||
}
|
||
|
||
function refresh(): Promise<TData | undefined> {
|
||
return run(...lastParams)
|
||
}
|
||
|
||
function cancel(): void {
|
||
controller?.abort()
|
||
}
|
||
|
||
function mutate(next: TData | ((prev: TData | undefined) => TData)): void {
|
||
data.value = typeof next === 'function' ? (next as (prev: TData | undefined) => TData)(data.value) : next
|
||
}
|
||
|
||
if (refreshDeps) {
|
||
watch(refreshDeps, () => {
|
||
refresh()
|
||
})
|
||
}
|
||
|
||
if (!manual) {
|
||
onMounted(() => {
|
||
run(...defaultParams)
|
||
})
|
||
}
|
||
|
||
onScopeDispose(() => {
|
||
controller?.abort()
|
||
})
|
||
|
||
return { data, loading, error, run, refresh, cancel, mutate }
|
||
}
|
||
|
||
/** 判断是否 axios 主动取消(AbortController)。 */
|
||
function isCanceled(e: unknown): boolean {
|
||
return isCancel(e)
|
||
}
|