Vue3通用组合式函数:UseLoadingFn

63 阅读1分钟

使用

const req1 = (ctx: {id: string}) => {
    return new Promise((resolve) => setTimeout(resolve, 100))
}

// 数组解构
const [req, {isLoading, result, error, isLoaded}] = useLoadingFn(req1, {
    key: ({ id }) => id
})
// 发起请求 req({id:1}) req({id:2})

// 单行判断正在加载(未设置key时): if(isLoading())

// 多行判断正在加载
// id === 1 时: if(isLoading(1)) 
// id === 2 时: if(isLoading(2))

// 判断是否第一次加载完毕 isLoaded, 同isLoading

// 对象
const req = useLoadingFn(req1, {
    key: ({ id }) => id
})

// 发起请求 req({id:1}) req({id:2})

// 单行判断正在加载(未设置key时): if(req.reqIsLoading())

// 多行判断正在加载
// id === 1 时: if(req.reqIsLoading(1)) 
// id === 2 时: if(req.reqIsLoading(2))

// 判断是否第一次加载完毕 req.isLoaded, 同req.isLoading

定义

import type { Ref } from 'vue'

export type UseLoadingFnKey = PropertyKey

type TruthyFn = (key?: UseLoadingFnKey) => boolean

type Result<T extends AnyFn> = Ref<Awaited<ReturnType<T>> | undefined>

export type UseLoadingFnReturn<T extends AnyFn> = [
  UseLoadingFnReturn<T>,
  {
    error: Ref<any>
    isLoaded: TruthyFn
    isLoading: TruthyFn
    result: Result<T>
  }
] &
  T & {
    error: Ref<any>
    isLoaded: TruthyFn
    isLoading: TruthyFn
    result: Result<T>
  }

export type UseLoadingFnOptions<T extends AnyFn> = {
  key?: (...args: Parameters<T>) => UseLoadingFnKey
}

const SINGLE_KEY = '__single'

export function useLoadingFn<T extends AnyFn>(
  fn: T,
  options?: UseLoadingFnOptions<NoInfer<T>>,
): UseLoadingFnReturn<NoInfer<T>> {
  const loadingMap = reactive(new Map<UseLoadingFnKey, boolean>())
  const result: Result<T> = ref()
  const error = ref<any>()
  const loadedMap = reactive(new Map<UseLoadingFnKey, boolean>())

  const loadingFn = (async (...args) => {
    const key =
      options?.key?.(...(args as Parameters<T>)) ??
      (SINGLE_KEY as UseLoadingFnKey)
    try {
      loadingMap.set(key, true)
      const ret = await fn(...args)
      result.value = ret
      return ret
    } catch (_error: any) {
      error.value = new Error(_error.message ?? _error.msg)
      throw _error
    } finally {
      loadedMap.set(key, true)
      loadingMap.set(key, false)
    }
  }) as UseLoadingFnReturn<T>

  const isLoading = (key: UseLoadingFnKey = SINGLE_KEY) => {
    if (loadingMap.has(key)) {
      return loadingMap.get(key) ?? false
    }
    return false
  }

  const isLoaded = (key: UseLoadingFnKey = SINGLE_KEY) => {
    if (loadedMap.has(key)) {
      return loadedMap.get(key) ?? false
    }
    return false
  }

  const arr = [loadingFn, {isLoading, result, error, isLoaded}] as const

  loadingFn[Symbol.iterator] = arr[Symbol.iterator].bind(arr)

  loadingFn.isLoaded = isLoaded
  loadingFn.isLoading = isLoading
  loadingFn.result = result
  loadingFn.error = error

  return loadingFn
}