import { useState, useEffect, useCallback, useRef } from 'react' /** 简易 AbortSignal 替代(兼容小程序环境) */ class SimpleAbortSignal { private _aborted = false get aborted() { return this._aborted } abort() { this._aborted = true } } class SimpleAbortController { signal = new SimpleAbortSignal() abort() { this.signal.abort() } } export interface RequestState { /** 请求结果数据 */ data: T | null /** 是否加载中 */ loading: boolean /** 错误信息 */ error: Error | null /** 是否首次加载完成(不管成功或失败) */ initialized: boolean } export interface UseRequestOptions { /** 手动触发模式下需要调用 run 执行 */ manual?: boolean /** 初始数据 */ defaultData?: T /** 请求函数 */ service?: (...args: P) => Promise /** 成功回调 */ onSuccess?: (data: T, params: P) => void /** 失败回调 */ onError?: (error: Error, params: P) => void /** 轮询间隔(毫秒),设置后自动轮询 */ pollingInterval?: number /** 轮询在页面隐藏时暂停,默认 true */ pollingWhenHidden?: boolean /** 防抖延迟(毫秒) */ debounceInterval?: number /** 依赖变化时自动重新请求 */ refreshDeps?: unknown[] /** 请求节流间隔(毫秒) */ throttleInterval?: number /** 缓存 key,相同 key 的请求结果会被缓存 */ cacheKey?: string /** 缓存有效期(毫秒) */ cacheTime?: number /** 重新请求尝试次数 */ retryCount?: number /** 重试延迟(毫秒),默认 1000 */ retryInterval?: number } export interface UseRequestReturn { /** 当前状态 */ state: RequestState /** 请求结果数据(state.data 的快捷访问) */ data: T | null /** 是否加载中(state.loading 的快捷访问) */ loading: boolean /** 错误信息(state.error 的快捷访问) */ error: Error | null /** 是否首次加载完成(state.initialized 的快捷访问) */ initialized: boolean /** 触发请求(manual 模式下) */ run: (...params: P) => Promise /** 带 loading 的运行 */ runLoading: (...params: P) => Promise /** 取消请求 */ cancel: () => void /** 刷新(使用上次参数) */ refresh: () => void /** 修改 data(不发起请求) */ mutate: (data: T | ((prev: T | null) => T)) => void /** 当前参数 */ params: P } // 简单缓存 const cache = new Map() function getCache(key: string): T | null { const item = cache.get(key) if (!item) return null if (Date.now() > item.expireAt) { cache.delete(key) return null } return item.data as T } function setCache(key: string, data: T, cacheTime: number) { cache.set(key, { data, expireAt: Date.now() + cacheTime }) } /** * 请求封装 Hook * @param service 请求函数 * @param options 配置选项 */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export function useRequest( service: (...args: P) => Promise, options: UseRequestOptions = {} ): UseRequestReturn // eslint-disable-next-line @typescript-eslint/no-explicit-any export function useRequest( service: undefined, options: UseRequestOptions = {} ): UseRequestReturn export function useRequest( service: ((...args: P) => Promise) | undefined, options: UseRequestOptions = {} ): UseRequestReturn { const { manual = false, defaultData, onSuccess, onError, pollingInterval, pollingWhenHidden = true, debounceInterval, refreshDeps = [], throttleInterval, cacheKey, cacheTime = 5 * 60 * 1000, retryCount = 0, retryInterval = 1000, } = options const [state, setState] = useState>({ data: defaultData ?? null, loading: false, error: null, initialized: !!defaultData, }) const [params, setParams] = useState

([] as unknown as P) const serviceRef = useRef(service) const onSuccessRef = useRef(onSuccess) const onErrorRef = useRef(onError) const pollingTimerRef = useRef | null>(null) const abortControllerRef = useRef(null) const isMountedRef = useRef(true) const debounceTimerRef = useRef | null>(null) const throttleLastRunRef = useRef(0) // 更新 refs useEffect(() => { serviceRef.current = service }, [service]) useEffect(() => { onSuccessRef.current = onSuccess }, [onSuccess]) useEffect(() => { onErrorRef.current = onError }, [onError]) // 清理 useEffect(() => { isMountedRef.current = true return () => { isMountedRef.current = false if (pollingTimerRef.current) clearInterval(pollingTimerRef.current) if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current) abortControllerRef.current?.abort() } }, []) // 页面可见性处理(使用 Taro 生命周期,兼容小程序环境) useEffect(() => { if (!pollingInterval || !pollingWhenHidden) return // 在小程序中,Taro.useDidShow / useDidHide 仅可在页面组件中使用 // useRequest 作为通用 hook 无法直接使用这些,因此跳过 // 如需轮询暂停/恢复能力,可在页面组件中手动调用 cancel / refresh }, [pollingInterval, pollingWhenHidden]) // 依赖刷新 - 使用 useRef 避免闭包过期 const refreshDepsRef = useRef(refreshDeps) refreshDepsRef.current = refreshDeps useEffect(() => { if (!manual && refreshDepsRef.current.length > 0) { refresh() } }, [...refreshDeps]) // eslint-disable-line const run = useCallback(async (...p: P): Promise => { // 防抖 if (debounceInterval) { if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current) return new Promise((resolve) => { debounceTimerRef.current = setTimeout(async () => { const result = await doRequest(...p) resolve(result) }, debounceInterval) }) } // 节流 if (throttleInterval) { const now = Date.now() if (now - throttleLastRunRef.current < throttleInterval) { return } throttleLastRunRef.current = now } return doRequest(...p) }, [debounceInterval, throttleInterval]) // eslint-disable-line const doRequest = useCallback(async (...p: P): Promise => { if (!serviceRef.current) return setParams(p) abortControllerRef.current?.abort() abortControllerRef.current = new SimpleAbortController() // 检查缓存 if (cacheKey) { const cached = getCache(cacheKey) if (cached !== null) { setState({ data: cached, loading: false, error: null, initialized: true }) return cached } } setState(prev => ({ ...prev, loading: true, error: null })) try { let result!: T let attempt = 0 const maxAttempts = retryCount + 1 while (attempt < maxAttempts) { try { result = await serviceRef.current(...p) break } catch (err: unknown) { attempt++ const isAbortError = err instanceof Error && err.message.includes('abort') if (attempt >= maxAttempts || isAbortError) { throw err } await new Promise(r => setTimeout(r, retryInterval * attempt)) } } if (!isMountedRef.current) return // 缓存 if (cacheKey) { setCache(cacheKey, result, cacheTime) } setState({ data: result, loading: false, error: null, initialized: true }) onSuccessRef.current?.(result, p) // 轮询 if (pollingInterval && !pollingTimerRef.current) { pollingTimerRef.current = setInterval(() => { doRequest(...p) }, pollingInterval) } return result } catch (err: unknown) { if (!isMountedRef.current) return const error = err instanceof Error ? err : new Error(String(err)) setState({ data: null, loading: false, error, initialized: true }) onErrorRef.current?.(error, p) return undefined } }, [cacheKey, cacheTime, pollingInterval, pollingWhenHidden, retryCount, retryInterval]) // eslint-disable-line const runLoading = useCallback(async (...p: P) => { if (!serviceRef.current) return setParams(p) return doRequest(...p) }, [doRequest]) const cancel = useCallback(() => { if (pollingTimerRef.current) { clearInterval(pollingTimerRef.current) pollingTimerRef.current = null } abortControllerRef.current?.abort() setState(prev => ({ ...prev, loading: false })) }, []) const refresh = useCallback(() => { if (params.length > 0) { run(...params) } }, [params, run]) const mutate = useCallback((data: T | ((prev: T | null) => T)) => { setState(prev => ({ ...prev, data: typeof data === 'function' ? (data as (prev: T | null) => T)(prev.data) : data, })) }, []) // 自动执行 useEffect(() => { if (!manual && service) { run(...([] as unknown as P)) } }, []) // eslint-disable-line return { state, data: state.data, loading: state.loading, error: state.error, initialized: state.initialized, run, runLoading, cancel, refresh, mutate, params, } }