committed by
GitHub
37 changed files with 2357 additions and 65 deletions
@ -1,6 +1,7 @@ |
|||||
export * from './onMountedOrActivated'; |
export * from './onMountedOrActivated'; |
||||
export * from './useAttrs'; |
export * from './useAttrs'; |
||||
export * from './useRefs'; |
export * from './useRefs'; |
||||
|
export * from './useRequest'; |
||||
export * from './useScrollTo'; |
export * from './useScrollTo'; |
||||
export * from './useWindowSizeFn'; |
export * from './useWindowSizeFn'; |
||||
export { useTimeoutFn } from '@vueuse/core'; |
export { useTimeoutFn } from '@vueuse/core'; |
||||
|
|||||
@ -0,0 +1,147 @@ |
|||||
|
/* eslint-disable @typescript-eslint/ban-ts-comment */ |
||||
|
import { reactive } from 'vue'; |
||||
|
|
||||
|
import type { FetchState, PluginReturn, Service, Subscribe, UseRequestOptions } from './types'; |
||||
|
import { isFunction } from './utils/isFunction'; |
||||
|
|
||||
|
export default class Fetch<TData, TParams extends any[]> { |
||||
|
pluginImpls: PluginReturn<TData, TParams>[] = []; |
||||
|
|
||||
|
count: number = 0; |
||||
|
|
||||
|
state: FetchState<TData, TParams> = reactive({ |
||||
|
loading: false, |
||||
|
params: undefined, |
||||
|
data: undefined, |
||||
|
error: undefined, |
||||
|
}); |
||||
|
|
||||
|
constructor( |
||||
|
public serviceRef: Service<TData, TParams>, |
||||
|
public options: UseRequestOptions<TData, TParams>, |
||||
|
public subscribe: Subscribe, |
||||
|
public initState: Partial<FetchState<TData, TParams>> = {}, |
||||
|
) { |
||||
|
this.setState({ loading: !options.manual, ...initState }); |
||||
|
} |
||||
|
|
||||
|
setState(s: Partial<FetchState<TData, TParams>> = {}) { |
||||
|
Object.assign(this.state, s); |
||||
|
this.subscribe(); |
||||
|
} |
||||
|
|
||||
|
runPluginHandler(event: keyof PluginReturn<TData, TParams>, ...rest: any[]) { |
||||
|
// @ts-ignore
|
||||
|
const r = this.pluginImpls.map((i) => i[event]?.(...rest)).filter(Boolean); |
||||
|
return Object.assign({}, ...r); |
||||
|
} |
||||
|
|
||||
|
async runAsync(...params: TParams): Promise<TData> { |
||||
|
this.count += 1; |
||||
|
const currentCount = this.count; |
||||
|
|
||||
|
const { |
||||
|
stopNow = false, |
||||
|
returnNow = false, |
||||
|
...state |
||||
|
} = this.runPluginHandler('onBefore', params); |
||||
|
|
||||
|
// stop request
|
||||
|
if (stopNow) { |
||||
|
return new Promise(() => {}); |
||||
|
} |
||||
|
|
||||
|
this.setState({ |
||||
|
loading: true, |
||||
|
params, |
||||
|
...state, |
||||
|
}); |
||||
|
|
||||
|
// return now
|
||||
|
if (returnNow) { |
||||
|
return Promise.resolve(state.data); |
||||
|
} |
||||
|
|
||||
|
this.options.onBefore?.(params); |
||||
|
|
||||
|
try { |
||||
|
// replace service
|
||||
|
let { servicePromise } = this.runPluginHandler('onRequest', this.serviceRef, params); |
||||
|
|
||||
|
if (!servicePromise) { |
||||
|
servicePromise = this.serviceRef(...params); |
||||
|
} |
||||
|
|
||||
|
const res = await servicePromise; |
||||
|
|
||||
|
if (currentCount !== this.count) { |
||||
|
// prevent run.then when request is canceled
|
||||
|
return new Promise(() => {}); |
||||
|
} |
||||
|
|
||||
|
// const formattedResult = this.options.formatResultRef.current ? this.options.formatResultRef.current(res) : res;
|
||||
|
|
||||
|
this.setState({ data: res, error: undefined, loading: false }); |
||||
|
|
||||
|
this.options.onSuccess?.(res, params); |
||||
|
this.runPluginHandler('onSuccess', res, params); |
||||
|
|
||||
|
this.options.onFinally?.(params, res, undefined); |
||||
|
|
||||
|
if (currentCount === this.count) { |
||||
|
this.runPluginHandler('onFinally', params, res, undefined); |
||||
|
} |
||||
|
|
||||
|
return res; |
||||
|
} catch (error) { |
||||
|
if (currentCount !== this.count) { |
||||
|
// prevent run.then when request is canceled
|
||||
|
return new Promise(() => {}); |
||||
|
} |
||||
|
|
||||
|
this.setState({ error, loading: false }); |
||||
|
|
||||
|
this.options.onError?.(error, params); |
||||
|
this.runPluginHandler('onError', error, params); |
||||
|
|
||||
|
this.options.onFinally?.(params, undefined, error); |
||||
|
|
||||
|
if (currentCount === this.count) { |
||||
|
this.runPluginHandler('onFinally', params, undefined, error); |
||||
|
} |
||||
|
|
||||
|
throw error; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
run(...params: TParams) { |
||||
|
this.runAsync(...params).catch((error) => { |
||||
|
if (!this.options.onError) { |
||||
|
console.error(error); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
cancel() { |
||||
|
this.count += 1; |
||||
|
this.setState({ loading: false }); |
||||
|
|
||||
|
this.runPluginHandler('onCancel'); |
||||
|
} |
||||
|
|
||||
|
refresh() { |
||||
|
// @ts-ignore
|
||||
|
this.run(...(this.state.params || [])); |
||||
|
} |
||||
|
|
||||
|
refreshAsync() { |
||||
|
// @ts-ignore
|
||||
|
return this.runAsync(...(this.state.params || [])); |
||||
|
} |
||||
|
|
||||
|
mutate(data?: TData | ((oldData?: TData) => TData | undefined)) { |
||||
|
const targetData = isFunction(data) ? data(this.state.data) : data; |
||||
|
this.runPluginHandler('onMutate', targetData); |
||||
|
this.setState({ data: targetData }); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,30 @@ |
|||||
|
import useAutoRunPlugin from './plugins/useAutoRunPlugin'; |
||||
|
import useCachePlugin from './plugins/useCachePlugin'; |
||||
|
import useDebouncePlugin from './plugins/useDebouncePlugin'; |
||||
|
import useLoadingDelayPlugin from './plugins/useLoadingDelayPlugin'; |
||||
|
import usePollingPlugin from './plugins/usePollingPlugin'; |
||||
|
import useRefreshOnWindowFocusPlugin from './plugins/useRefreshOnWindowFocusPlugin'; |
||||
|
import useRetryPlugin from './plugins/useRetryPlugin'; |
||||
|
import useThrottlePlugin from './plugins/useThrottlePlugin'; |
||||
|
import type { Service, UseRequestOptions, UseRequestPlugin } from './types'; |
||||
|
import { useRequestImplement } from './useRequestImplement'; |
||||
|
|
||||
|
export { clearCache } from './utils/cache'; |
||||
|
|
||||
|
export function useRequest<TData, TParams extends any[]>( |
||||
|
service: Service<TData, TParams>, |
||||
|
options?: UseRequestOptions<TData, TParams>, |
||||
|
plugins?: UseRequestPlugin<TData, TParams>[], |
||||
|
) { |
||||
|
return useRequestImplement<TData, TParams>(service, options, [ |
||||
|
...(plugins || []), |
||||
|
useDebouncePlugin, |
||||
|
useLoadingDelayPlugin, |
||||
|
usePollingPlugin, |
||||
|
useRefreshOnWindowFocusPlugin, |
||||
|
useThrottlePlugin, |
||||
|
useAutoRunPlugin, |
||||
|
useCachePlugin, |
||||
|
useRetryPlugin, |
||||
|
] as UseRequestPlugin<TData, TParams>[]); |
||||
|
} |
||||
@ -0,0 +1,52 @@ |
|||||
|
import { ref, unref, watch } from 'vue'; |
||||
|
|
||||
|
import type { UseRequestPlugin } from '../types'; |
||||
|
|
||||
|
// support refreshDeps & ready
|
||||
|
const useAutoRunPlugin: UseRequestPlugin<any, any[]> = ( |
||||
|
fetchInstance, |
||||
|
{ manual, ready = true, defaultParams = [], refreshDeps = [], refreshDepsAction }, |
||||
|
) => { |
||||
|
const hasAutoRun = ref(false); |
||||
|
|
||||
|
watch( |
||||
|
() => unref(ready), |
||||
|
(readyVal) => { |
||||
|
if (!unref(manual) && readyVal) { |
||||
|
hasAutoRun.value = true; |
||||
|
fetchInstance.run(...defaultParams); |
||||
|
} |
||||
|
}, |
||||
|
); |
||||
|
|
||||
|
if (refreshDeps.length) { |
||||
|
watch(refreshDeps, () => { |
||||
|
if (hasAutoRun.value) { |
||||
|
return; |
||||
|
} |
||||
|
if (!manual) { |
||||
|
if (refreshDepsAction) { |
||||
|
refreshDepsAction(); |
||||
|
} else { |
||||
|
fetchInstance.refresh(); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
onBefore: () => { |
||||
|
if (!unref(ready)) { |
||||
|
return { stopNow: true }; |
||||
|
} |
||||
|
}, |
||||
|
}; |
||||
|
}; |
||||
|
|
||||
|
useAutoRunPlugin.onInit = ({ ready = true, manual }) => { |
||||
|
return { |
||||
|
loading: !unref(manual) && unref(ready), |
||||
|
}; |
||||
|
}; |
||||
|
|
||||
|
export default useAutoRunPlugin; |
||||
@ -0,0 +1,127 @@ |
|||||
|
import { onUnmounted, ref, watchEffect } from 'vue'; |
||||
|
|
||||
|
import type { UseRequestPlugin } from '../types'; |
||||
|
import type { CachedData } from '../utils/cache'; |
||||
|
import { getCache, setCache } from '../utils/cache'; |
||||
|
import { getCachePromise, setCachePromise } from '../utils/cachePromise'; |
||||
|
import { subscribe, trigger } from '../utils/cacheSubscribe'; |
||||
|
|
||||
|
const useCachePlugin: UseRequestPlugin<any, any[]> = ( |
||||
|
fetchInstance, |
||||
|
{ |
||||
|
cacheKey, |
||||
|
cacheTime = 5 * 60 * 1000, |
||||
|
staleTime = 0, |
||||
|
setCache: customSetCache, |
||||
|
getCache: customGetCache, |
||||
|
}, |
||||
|
) => { |
||||
|
const unSubscribeRef = ref<() => void>(); |
||||
|
const currentPromiseRef = ref<Promise<any>>(); |
||||
|
|
||||
|
const _setCache = (key: string, cachedData: CachedData) => { |
||||
|
customSetCache ? customSetCache(cachedData) : setCache(key, cacheTime, cachedData); |
||||
|
trigger(key, cachedData.data); |
||||
|
}; |
||||
|
|
||||
|
const _getCache = (key: string, params: any[] = []) => { |
||||
|
return customGetCache ? customGetCache(params) : getCache(key); |
||||
|
}; |
||||
|
|
||||
|
watchEffect(() => { |
||||
|
if (!cacheKey) return; |
||||
|
|
||||
|
// get data from cache when init
|
||||
|
const cacheData = _getCache(cacheKey); |
||||
|
if (cacheData && Object.hasOwnProperty.call(cacheData, 'data')) { |
||||
|
fetchInstance.state.data = cacheData.data; |
||||
|
fetchInstance.state.params = cacheData.params; |
||||
|
|
||||
|
if (staleTime === -1 || new Date().getTime() - cacheData.time <= staleTime) { |
||||
|
fetchInstance.state.loading = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// subscribe same cachekey update, trigger update
|
||||
|
unSubscribeRef.value = subscribe(cacheKey, (data) => { |
||||
|
fetchInstance.setState({ data }); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
onUnmounted(() => { |
||||
|
unSubscribeRef.value?.(); |
||||
|
}); |
||||
|
|
||||
|
if (!cacheKey) { |
||||
|
return {}; |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
onBefore: (params) => { |
||||
|
const cacheData = _getCache(cacheKey, params); |
||||
|
|
||||
|
if (!cacheData || !Object.hasOwnProperty.call(cacheData, 'data')) { |
||||
|
return {}; |
||||
|
} |
||||
|
|
||||
|
// If the data is fresh, stop request
|
||||
|
if (staleTime === -1 || new Date().getTime() - cacheData.time <= staleTime) { |
||||
|
return { |
||||
|
loading: false, |
||||
|
data: cacheData?.data, |
||||
|
error: undefined, |
||||
|
returnNow: true, |
||||
|
}; |
||||
|
} else { |
||||
|
// If the data is stale, return data, and request continue
|
||||
|
return { data: cacheData?.data, error: undefined }; |
||||
|
} |
||||
|
}, |
||||
|
onRequest: (service, args) => { |
||||
|
let servicePromise = getCachePromise(cacheKey); |
||||
|
|
||||
|
// If has servicePromise, and is not trigger by self, then use it
|
||||
|
if (servicePromise && servicePromise !== currentPromiseRef.value) { |
||||
|
return { servicePromise }; |
||||
|
} |
||||
|
|
||||
|
servicePromise = service(...args); |
||||
|
currentPromiseRef.value = servicePromise; |
||||
|
setCachePromise(cacheKey, servicePromise); |
||||
|
|
||||
|
return { servicePromise }; |
||||
|
}, |
||||
|
onSuccess: (data, params) => { |
||||
|
if (cacheKey) { |
||||
|
// cancel subscribe, avoid trgger self
|
||||
|
unSubscribeRef.value?.(); |
||||
|
|
||||
|
_setCache(cacheKey, { data, params, time: new Date().getTime() }); |
||||
|
|
||||
|
// resubscribe
|
||||
|
unSubscribeRef.value = subscribe(cacheKey, (d) => { |
||||
|
fetchInstance.setState({ data: d }); |
||||
|
}); |
||||
|
} |
||||
|
}, |
||||
|
onMutate: (data) => { |
||||
|
if (cacheKey) { |
||||
|
// cancel subscribe, avoid trigger self
|
||||
|
unSubscribeRef.value?.(); |
||||
|
|
||||
|
_setCache(cacheKey, { |
||||
|
data, |
||||
|
params: fetchInstance.state.params, |
||||
|
time: new Date().getTime(), |
||||
|
}); |
||||
|
|
||||
|
// resubscribe
|
||||
|
unSubscribeRef.value = subscribe(cacheKey, (d) => { |
||||
|
fetchInstance.setState({ data: d }); |
||||
|
}); |
||||
|
} |
||||
|
}, |
||||
|
}; |
||||
|
}; |
||||
|
|
||||
|
export default useCachePlugin; |
||||
@ -0,0 +1,71 @@ |
|||||
|
import type { DebouncedFunc, DebounceSettings } from 'lodash-es'; |
||||
|
import { debounce } from 'lodash-es'; |
||||
|
import { computed, ref, watchEffect } from 'vue'; |
||||
|
|
||||
|
import type { UseRequestPlugin } from '../types'; |
||||
|
|
||||
|
const useDebouncePlugin: UseRequestPlugin<any, any[]> = ( |
||||
|
fetchInstance, |
||||
|
{ debounceWait, debounceLeading, debounceTrailing, debounceMaxWait }, |
||||
|
) => { |
||||
|
const debouncedRef = ref<DebouncedFunc<any>>(); |
||||
|
|
||||
|
const options = computed(() => { |
||||
|
const ret: DebounceSettings = {}; |
||||
|
|
||||
|
if (debounceLeading !== undefined) { |
||||
|
ret.leading = debounceLeading; |
||||
|
} |
||||
|
if (debounceTrailing !== undefined) { |
||||
|
ret.trailing = debounceTrailing; |
||||
|
} |
||||
|
if (debounceMaxWait !== undefined) { |
||||
|
ret.maxWait = debounceMaxWait; |
||||
|
} |
||||
|
|
||||
|
return ret; |
||||
|
}); |
||||
|
|
||||
|
watchEffect(() => { |
||||
|
if (debounceWait) { |
||||
|
const _originRunAsync = fetchInstance.runAsync.bind(fetchInstance); |
||||
|
|
||||
|
debouncedRef.value = debounce( |
||||
|
(callback) => { |
||||
|
callback(); |
||||
|
}, |
||||
|
debounceWait, |
||||
|
options.value, |
||||
|
); |
||||
|
|
||||
|
// debounce runAsync should be promise
|
||||
|
// https://github.com/lodash/lodash/issues/4400#issuecomment-834800398
|
||||
|
fetchInstance.runAsync = (...args) => { |
||||
|
return new Promise((resolve, reject) => { |
||||
|
debouncedRef.value?.(() => { |
||||
|
_originRunAsync(...args) |
||||
|
.then(resolve) |
||||
|
.catch(reject); |
||||
|
}); |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
return () => { |
||||
|
debouncedRef.value?.cancel(); |
||||
|
fetchInstance.runAsync = _originRunAsync; |
||||
|
}; |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
if (!debounceWait) { |
||||
|
return {}; |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
onCancel: () => { |
||||
|
debouncedRef.value?.cancel(); |
||||
|
}, |
||||
|
}; |
||||
|
}; |
||||
|
|
||||
|
export default useDebouncePlugin; |
||||
@ -0,0 +1,45 @@ |
|||||
|
import { ref, unref } from 'vue'; |
||||
|
|
||||
|
import type { UseRequestPlugin, UseRequestTimeout } from '../types'; |
||||
|
|
||||
|
const useLoadingDelayPlugin: UseRequestPlugin<any, any[]> = ( |
||||
|
fetchInstance, |
||||
|
{ loadingDelay, ready }, |
||||
|
) => { |
||||
|
const timerRef = ref<UseRequestTimeout>(); |
||||
|
|
||||
|
if (!loadingDelay) { |
||||
|
return {}; |
||||
|
} |
||||
|
|
||||
|
const cancelTimeout = () => { |
||||
|
if (timerRef.value) { |
||||
|
clearTimeout(timerRef.value); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
return { |
||||
|
onBefore: () => { |
||||
|
cancelTimeout(); |
||||
|
|
||||
|
// Two cases:
|
||||
|
// 1. ready === undefined
|
||||
|
// 2. ready === true
|
||||
|
if (unref(ready) !== false) { |
||||
|
timerRef.value = setTimeout(() => { |
||||
|
fetchInstance.setState({ loading: true }); |
||||
|
}, loadingDelay); |
||||
|
} |
||||
|
|
||||
|
return { loading: false }; |
||||
|
}, |
||||
|
onFinally: () => { |
||||
|
cancelTimeout(); |
||||
|
}, |
||||
|
onCancel: () => { |
||||
|
cancelTimeout(); |
||||
|
}, |
||||
|
}; |
||||
|
}; |
||||
|
|
||||
|
export default useLoadingDelayPlugin; |
||||
@ -0,0 +1,71 @@ |
|||||
|
import { ref, watch } from 'vue'; |
||||
|
|
||||
|
import type { UseRequestPlugin, UseRequestTimeout } from '../types'; |
||||
|
import { isDocumentVisible } from '../utils/isDocumentVisible'; |
||||
|
import subscribeReVisible from '../utils/subscribeReVisible'; |
||||
|
|
||||
|
const usePollingPlugin: UseRequestPlugin<any, any[]> = ( |
||||
|
fetchInstance, |
||||
|
{ pollingInterval, pollingWhenHidden = true, pollingErrorRetryCount = -1 }, |
||||
|
) => { |
||||
|
const timerRef = ref<UseRequestTimeout>(); |
||||
|
const unsubscribeRef = ref<() => void>(); |
||||
|
const countRef = ref<number>(0); |
||||
|
|
||||
|
const stopPolling = () => { |
||||
|
if (timerRef.value) { |
||||
|
clearTimeout(timerRef.value); |
||||
|
} |
||||
|
unsubscribeRef.value?.(); |
||||
|
}; |
||||
|
|
||||
|
watch( |
||||
|
() => pollingInterval, |
||||
|
() => { |
||||
|
if (!pollingInterval) { |
||||
|
stopPolling(); |
||||
|
} |
||||
|
}, |
||||
|
); |
||||
|
|
||||
|
if (!pollingInterval) { |
||||
|
return {}; |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
onBefore: () => { |
||||
|
stopPolling(); |
||||
|
}, |
||||
|
onError: () => { |
||||
|
countRef.value += 1; |
||||
|
}, |
||||
|
onSuccess: () => { |
||||
|
countRef.value = 0; |
||||
|
}, |
||||
|
onFinally: () => { |
||||
|
if ( |
||||
|
pollingErrorRetryCount === -1 || |
||||
|
// When an error occurs, the request is not repeated after pollingErrorRetryCount retries
|
||||
|
(pollingErrorRetryCount !== -1 && countRef.value <= pollingErrorRetryCount) |
||||
|
) { |
||||
|
timerRef.value = setTimeout(() => { |
||||
|
// if pollingWhenHidden = false && document is hidden, then stop polling and subscribe revisible
|
||||
|
if (!pollingWhenHidden && !isDocumentVisible()) { |
||||
|
unsubscribeRef.value = subscribeReVisible(() => { |
||||
|
fetchInstance.refresh(); |
||||
|
}); |
||||
|
} else { |
||||
|
fetchInstance.refresh(); |
||||
|
} |
||||
|
}, pollingInterval); |
||||
|
} else { |
||||
|
countRef.value = 0; |
||||
|
} |
||||
|
}, |
||||
|
onCancel: () => { |
||||
|
stopPolling(); |
||||
|
}, |
||||
|
}; |
||||
|
}; |
||||
|
|
||||
|
export default usePollingPlugin; |
||||
@ -0,0 +1,37 @@ |
|||||
|
import { onUnmounted, ref, watchEffect } from 'vue'; |
||||
|
|
||||
|
import type { UseRequestPlugin } from '../types'; |
||||
|
import { limit } from '../utils/limit'; |
||||
|
import subscribeFocus from '../utils/subscribeFocus'; |
||||
|
|
||||
|
const useRefreshOnWindowFocusPlugin: UseRequestPlugin<any, any[]> = ( |
||||
|
fetchInstance, |
||||
|
{ refreshOnWindowFocus, focusTimespan = 5000 }, |
||||
|
) => { |
||||
|
const unsubscribeRef = ref<() => void>(); |
||||
|
|
||||
|
const stopSubscribe = () => { |
||||
|
unsubscribeRef.value?.(); |
||||
|
}; |
||||
|
|
||||
|
watchEffect(() => { |
||||
|
if (refreshOnWindowFocus) { |
||||
|
const limitRefresh = limit(fetchInstance.refresh.bind(fetchInstance), focusTimespan); |
||||
|
unsubscribeRef.value = subscribeFocus(() => { |
||||
|
limitRefresh(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
return () => { |
||||
|
stopSubscribe(); |
||||
|
}; |
||||
|
}); |
||||
|
|
||||
|
onUnmounted(() => { |
||||
|
stopSubscribe(); |
||||
|
}); |
||||
|
|
||||
|
return {}; |
||||
|
}; |
||||
|
|
||||
|
export default useRefreshOnWindowFocusPlugin; |
||||
@ -0,0 +1,54 @@ |
|||||
|
import { ref } from 'vue'; |
||||
|
|
||||
|
import type { UseRequestPlugin, UseRequestTimeout } from '../types'; |
||||
|
|
||||
|
const useRetryPlugin: UseRequestPlugin<any, any[]> = ( |
||||
|
fetchInstance, |
||||
|
{ retryInterval, retryCount }, |
||||
|
) => { |
||||
|
const timerRef = ref<UseRequestTimeout>(); |
||||
|
const countRef = ref(0); |
||||
|
|
||||
|
const triggerByRetry = ref(false); |
||||
|
|
||||
|
if (!retryCount) { |
||||
|
return {}; |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
onBefore: () => { |
||||
|
if (!triggerByRetry.value) { |
||||
|
countRef.value = 0; |
||||
|
} |
||||
|
triggerByRetry.value = false; |
||||
|
|
||||
|
if (timerRef.value) { |
||||
|
clearTimeout(timerRef.value); |
||||
|
} |
||||
|
}, |
||||
|
onSuccess: () => { |
||||
|
countRef.value = 0; |
||||
|
}, |
||||
|
onError: () => { |
||||
|
countRef.value += 1; |
||||
|
if (retryCount === -1 || countRef.value <= retryCount) { |
||||
|
// Exponential backoff
|
||||
|
const timeout = retryInterval ?? Math.min(1000 * 2 ** countRef.value, 30000); |
||||
|
timerRef.value = setTimeout(() => { |
||||
|
triggerByRetry.value = true; |
||||
|
fetchInstance.refresh(); |
||||
|
}, timeout); |
||||
|
} else { |
||||
|
countRef.value = 0; |
||||
|
} |
||||
|
}, |
||||
|
onCancel: () => { |
||||
|
countRef.value = 0; |
||||
|
if (timerRef.value) { |
||||
|
clearTimeout(timerRef.value); |
||||
|
} |
||||
|
}, |
||||
|
}; |
||||
|
}; |
||||
|
|
||||
|
export default useRetryPlugin; |
||||
@ -0,0 +1,63 @@ |
|||||
|
import type { DebouncedFunc, ThrottleSettings } from 'lodash-es'; |
||||
|
import { throttle } from 'lodash-es'; |
||||
|
import { ref, watchEffect } from 'vue'; |
||||
|
|
||||
|
import type { UseRequestPlugin } from '../types'; |
||||
|
|
||||
|
const useThrottlePlugin: UseRequestPlugin<any, any[]> = ( |
||||
|
fetchInstance, |
||||
|
{ throttleWait, throttleLeading, throttleTrailing }, |
||||
|
) => { |
||||
|
const throttledRef = ref<DebouncedFunc<any>>(); |
||||
|
|
||||
|
const options: ThrottleSettings = {}; |
||||
|
if (throttleLeading !== undefined) { |
||||
|
options.leading = throttleLeading; |
||||
|
} |
||||
|
if (throttleTrailing !== undefined) { |
||||
|
options.trailing = throttleTrailing; |
||||
|
} |
||||
|
|
||||
|
watchEffect(() => { |
||||
|
if (throttleWait) { |
||||
|
const _originRunAsync = fetchInstance.runAsync.bind(fetchInstance); |
||||
|
|
||||
|
throttledRef.value = throttle( |
||||
|
(callback) => { |
||||
|
callback(); |
||||
|
}, |
||||
|
throttleWait, |
||||
|
options, |
||||
|
); |
||||
|
|
||||
|
// throttle runAsync should be promise
|
||||
|
// https://github.com/lodash/lodash/issues/4400#issuecomment-834800398
|
||||
|
fetchInstance.runAsync = (...args) => { |
||||
|
return new Promise((resolve, reject) => { |
||||
|
throttledRef.value?.(() => { |
||||
|
_originRunAsync(...args) |
||||
|
.then(resolve) |
||||
|
.catch(reject); |
||||
|
}); |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
return () => { |
||||
|
fetchInstance.runAsync = _originRunAsync; |
||||
|
throttledRef.value?.cancel(); |
||||
|
}; |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
if (!throttleWait) { |
||||
|
return {}; |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
onCancel: () => { |
||||
|
throttledRef.value?.cancel(); |
||||
|
}, |
||||
|
}; |
||||
|
}; |
||||
|
|
||||
|
export default useThrottlePlugin; |
||||
@ -0,0 +1,124 @@ |
|||||
|
import type { MaybeRef, Ref, WatchSource } from 'vue'; |
||||
|
|
||||
|
import type Fetch from './Fetch'; |
||||
|
import type { CachedData } from './utils/cache'; |
||||
|
|
||||
|
export type Service<TData, TParams extends any[]> = (...args: TParams) => Promise<TData>; |
||||
|
export type Subscribe = () => void; |
||||
|
|
||||
|
// for Fetch
|
||||
|
export interface FetchState<TData, TParams extends any[]> { |
||||
|
loading: boolean; |
||||
|
params?: TParams; |
||||
|
data?: TData; |
||||
|
error?: Error; |
||||
|
} |
||||
|
|
||||
|
export interface PluginReturn<TData, TParams extends any[]> { |
||||
|
onBefore?: (params: TParams) => |
||||
|
| ({ |
||||
|
stopNow?: boolean; |
||||
|
returnNow?: boolean; |
||||
|
} & Partial<FetchState<TData, TParams>>) |
||||
|
| void; |
||||
|
|
||||
|
onRequest?: ( |
||||
|
service: Service<TData, TParams>, |
||||
|
params: TParams, |
||||
|
) => { |
||||
|
servicePromise?: Promise<TData>; |
||||
|
}; |
||||
|
|
||||
|
onSuccess?: (data: TData, params: TParams) => void; |
||||
|
onError?: (e: Error, params: TParams) => void; |
||||
|
onFinally?: (params: TParams, data?: TData, e?: Error) => void; |
||||
|
onCancel?: () => void; |
||||
|
onMutate?: (data: TData) => void; |
||||
|
} |
||||
|
|
||||
|
// for useRequestImplement
|
||||
|
export interface UseRequestOptions<TData, TParams extends any[]> { |
||||
|
manual?: MaybeRef<boolean>; |
||||
|
|
||||
|
onBefore?: (params: TParams) => void; |
||||
|
onSuccess?: (data: TData, params: TParams) => void; |
||||
|
onError?: (e: Error, params: TParams) => void; |
||||
|
// formatResult?: (res: any) => TData;
|
||||
|
onFinally?: (params: TParams, data?: TData, e?: Error) => void; |
||||
|
|
||||
|
defaultParams?: TParams; |
||||
|
|
||||
|
// refreshDeps
|
||||
|
refreshDeps?: WatchSource<any>[]; |
||||
|
refreshDepsAction?: () => void; |
||||
|
|
||||
|
// loading delay
|
||||
|
loadingDelay?: number; |
||||
|
|
||||
|
// polling
|
||||
|
pollingInterval?: number; |
||||
|
pollingWhenHidden?: boolean; |
||||
|
pollingErrorRetryCount?: number; |
||||
|
|
||||
|
// refresh on window focus
|
||||
|
refreshOnWindowFocus?: boolean; |
||||
|
focusTimespan?: number; |
||||
|
|
||||
|
// debounce
|
||||
|
debounceWait?: number; |
||||
|
debounceLeading?: boolean; |
||||
|
debounceTrailing?: boolean; |
||||
|
debounceMaxWait?: number; |
||||
|
|
||||
|
// throttle
|
||||
|
throttleWait?: number; |
||||
|
throttleLeading?: boolean; |
||||
|
throttleTrailing?: boolean; |
||||
|
|
||||
|
// cache
|
||||
|
cacheKey?: string; |
||||
|
cacheTime?: number; |
||||
|
staleTime?: number; |
||||
|
setCache?: (data: CachedData<TData, TParams>) => void; |
||||
|
getCache?: (params: TParams) => CachedData<TData, TParams> | undefined; |
||||
|
|
||||
|
// retry
|
||||
|
retryCount?: number; |
||||
|
retryInterval?: number; |
||||
|
|
||||
|
// ready
|
||||
|
ready?: MaybeRef<boolean>; |
||||
|
|
||||
|
// [key: string]: any;
|
||||
|
} |
||||
|
|
||||
|
export interface UseRequestPlugin<TData, TParams extends any[]> { |
||||
|
// eslint-disable-next-line prettier/prettier
|
||||
|
(fetchInstance: Fetch<TData, TParams>, options: UseRequestOptions<TData, TParams>): PluginReturn< |
||||
|
TData, |
||||
|
TParams |
||||
|
>; |
||||
|
onInit?: (options: UseRequestOptions<TData, TParams>) => Partial<FetchState<TData, TParams>>; |
||||
|
} |
||||
|
|
||||
|
// for index
|
||||
|
// export type OptionsWithoutFormat<TData, TParams extends any[]> = Omit<Options<TData, TParams>, 'formatResult'>;
|
||||
|
|
||||
|
// export interface OptionsWithFormat<TData, TParams extends any[], TFormated, TTFormated extends TFormated = any> extends Omit<Options<TTFormated, TParams>, 'formatResult'> {
|
||||
|
// formatResult: (res: TData) => TFormated;
|
||||
|
// };
|
||||
|
|
||||
|
export interface UseRequestResult<TData, TParams extends any[]> { |
||||
|
loading: Ref<boolean>; |
||||
|
data: Ref<TData>; |
||||
|
error: Ref<Error>; |
||||
|
params: Ref<TParams | []>; |
||||
|
cancel: Fetch<TData, TParams>['cancel']; |
||||
|
refresh: Fetch<TData, TParams>['refresh']; |
||||
|
refreshAsync: Fetch<TData, TParams>['refreshAsync']; |
||||
|
run: Fetch<TData, TParams>['run']; |
||||
|
runAsync: Fetch<TData, TParams>['runAsync']; |
||||
|
mutate: Fetch<TData, TParams>['mutate']; |
||||
|
} |
||||
|
|
||||
|
export type UseRequestTimeout = ReturnType<typeof setTimeout>; |
||||
@ -0,0 +1,49 @@ |
|||||
|
/* eslint-disable @typescript-eslint/ban-ts-comment */ |
||||
|
import { onMounted, onUnmounted, toRefs } from 'vue'; |
||||
|
|
||||
|
import Fetch from './Fetch'; |
||||
|
import type { Service, UseRequestOptions, UseRequestPlugin, UseRequestResult } from './types'; |
||||
|
|
||||
|
export function useRequestImplement<TData, TParams extends any[]>( |
||||
|
service: Service<TData, TParams>, |
||||
|
options: UseRequestOptions<TData, TParams> = {}, |
||||
|
plugins: UseRequestPlugin<TData, TParams>[] = [], |
||||
|
) { |
||||
|
const { manual = false, ...rest } = options; |
||||
|
const fetchOptions = { manual, ...rest }; |
||||
|
|
||||
|
const initState = plugins.map((p) => p?.onInit?.(fetchOptions)).filter(Boolean); |
||||
|
|
||||
|
const fetchInstance = new Fetch<TData, TParams>( |
||||
|
service, |
||||
|
fetchOptions, |
||||
|
() => {}, |
||||
|
Object.assign({}, ...initState), |
||||
|
); |
||||
|
|
||||
|
fetchInstance.options = fetchOptions; |
||||
|
// run all plugins hooks
|
||||
|
fetchInstance.pluginImpls = plugins.map((p) => p(fetchInstance, fetchOptions)); |
||||
|
|
||||
|
onMounted(() => { |
||||
|
if (!manual) { |
||||
|
const params = fetchInstance.state.params || options.defaultParams || []; |
||||
|
// @ts-ignore
|
||||
|
fetchInstance.run(...params); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
onUnmounted(() => { |
||||
|
fetchInstance.cancel(); |
||||
|
}); |
||||
|
|
||||
|
return { |
||||
|
...toRefs(fetchInstance.state), |
||||
|
cancel: fetchInstance.cancel.bind(fetchInstance), |
||||
|
mutate: fetchInstance.mutate.bind(fetchInstance), |
||||
|
refresh: fetchInstance.refresh.bind(fetchInstance), |
||||
|
refreshAsync: fetchInstance.refreshAsync.bind(fetchInstance), |
||||
|
run: fetchInstance.run.bind(fetchInstance), |
||||
|
runAsync: fetchInstance.runAsync.bind(fetchInstance), |
||||
|
} as UseRequestResult<TData, TParams>; |
||||
|
} |
||||
@ -0,0 +1,48 @@ |
|||||
|
type Timer = ReturnType<typeof setTimeout>; |
||||
|
type CachedKey = string | number; |
||||
|
|
||||
|
export interface CachedData<TData = any, TParams = any> { |
||||
|
data: TData; |
||||
|
params: TParams; |
||||
|
time: number; |
||||
|
} |
||||
|
|
||||
|
interface RecordData extends CachedData { |
||||
|
timer: Timer | undefined; |
||||
|
} |
||||
|
|
||||
|
const cache = new Map<CachedKey, RecordData>(); |
||||
|
|
||||
|
export const setCache = (key: CachedKey, cacheTime: number, cachedData: CachedData) => { |
||||
|
const currentCache = cache.get(key); |
||||
|
if (currentCache?.timer) { |
||||
|
clearTimeout(currentCache.timer); |
||||
|
} |
||||
|
|
||||
|
let timer: Timer | undefined = undefined; |
||||
|
|
||||
|
if (cacheTime > -1) { |
||||
|
// if cache out, clear it
|
||||
|
timer = setTimeout(() => { |
||||
|
cache.delete(key); |
||||
|
}, cacheTime); |
||||
|
} |
||||
|
|
||||
|
cache.set(key, { |
||||
|
...cachedData, |
||||
|
timer, |
||||
|
}); |
||||
|
}; |
||||
|
|
||||
|
export const getCache = (key: CachedKey) => { |
||||
|
return cache.get(key); |
||||
|
}; |
||||
|
|
||||
|
export const clearCache = (key?: string | string[]) => { |
||||
|
if (key) { |
||||
|
const cacheKeys = Array.isArray(key) ? key : [key]; |
||||
|
cacheKeys.forEach((cacheKey) => cache.delete(cacheKey)); |
||||
|
} else { |
||||
|
cache.clear(); |
||||
|
} |
||||
|
}; |
||||
@ -0,0 +1,23 @@ |
|||||
|
type CachedKey = string | number; |
||||
|
|
||||
|
const cachePromise = new Map<CachedKey, Promise<any>>(); |
||||
|
|
||||
|
export const getCachePromise = (cacheKey: CachedKey) => { |
||||
|
return cachePromise.get(cacheKey); |
||||
|
}; |
||||
|
|
||||
|
export const setCachePromise = (cacheKey: CachedKey, promise: Promise<any>) => { |
||||
|
// Should cache the same promise, cannot be promise.finally
|
||||
|
// Because the promise.finally will change the reference of the promise
|
||||
|
cachePromise.set(cacheKey, promise); |
||||
|
|
||||
|
// no use promise.finally for compatibility
|
||||
|
promise |
||||
|
.then((res) => { |
||||
|
cachePromise.delete(cacheKey); |
||||
|
return res; |
||||
|
}) |
||||
|
.catch(() => { |
||||
|
cachePromise.delete(cacheKey); |
||||
|
}); |
||||
|
}; |
||||
@ -0,0 +1,22 @@ |
|||||
|
type Listener = (data: any) => void; |
||||
|
|
||||
|
const listeners: Record<string, Listener[]> = {}; |
||||
|
|
||||
|
export const trigger = (key: string, data: any) => { |
||||
|
if (listeners[key]) { |
||||
|
listeners[key].forEach((item) => item(data)); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
export const subscribe = (key: string, listener: Listener) => { |
||||
|
if (!listeners[key]) { |
||||
|
listeners[key] = []; |
||||
|
} |
||||
|
|
||||
|
listeners[key].push(listener); |
||||
|
|
||||
|
return function unsubscribe() { |
||||
|
const index = listeners[key].indexOf(listener); |
||||
|
listeners[key].splice(index, 1); |
||||
|
}; |
||||
|
}; |
||||
@ -0,0 +1,5 @@ |
|||||
|
export const isBrowser = !!( |
||||
|
typeof window !== 'undefined' && |
||||
|
window.document && |
||||
|
window.document.createElement |
||||
|
); |
||||
@ -0,0 +1,8 @@ |
|||||
|
import { isBrowser } from './isBrowser'; |
||||
|
|
||||
|
export function isDocumentVisible(): boolean { |
||||
|
if (isBrowser) { |
||||
|
return document.visibilityState !== 'hidden'; |
||||
|
} |
||||
|
return true; |
||||
|
} |
||||
@ -0,0 +1,2 @@ |
|||||
|
export const isFunction = (value: unknown): value is (...args: any) => any => |
||||
|
typeof value === 'function'; |
||||
@ -0,0 +1,8 @@ |
|||||
|
import { isBrowser } from './isBrowser'; |
||||
|
|
||||
|
export function isOnline(): boolean { |
||||
|
if (isBrowser && typeof navigator.onLine !== 'undefined') { |
||||
|
return navigator.onLine; |
||||
|
} |
||||
|
return true; |
||||
|
} |
||||
@ -0,0 +1,12 @@ |
|||||
|
export function limit(fn: any, timespan: number) { |
||||
|
let pending = false; |
||||
|
|
||||
|
return (...args: any[]) => { |
||||
|
if (pending) return; |
||||
|
pending = true; |
||||
|
fn(...args); |
||||
|
setTimeout(() => { |
||||
|
pending = false; |
||||
|
}, timespan); |
||||
|
}; |
||||
|
} |
||||
@ -0,0 +1,30 @@ |
|||||
|
import { isBrowser } from './isBrowser'; |
||||
|
import { isDocumentVisible } from './isDocumentVisible'; |
||||
|
import { isOnline } from './isOnline'; |
||||
|
|
||||
|
type Listener = () => void; |
||||
|
|
||||
|
const listeners: Listener[] = []; |
||||
|
|
||||
|
if (isBrowser) { |
||||
|
const revalidate = () => { |
||||
|
if (!isDocumentVisible() || !isOnline()) return; |
||||
|
for (let i = 0; i < listeners.length; i++) { |
||||
|
const listener = listeners[i]; |
||||
|
listener(); |
||||
|
} |
||||
|
}; |
||||
|
window.addEventListener('visibilitychange', revalidate, false); |
||||
|
window.addEventListener('focus', revalidate, false); |
||||
|
} |
||||
|
|
||||
|
export default function subscribe(listener: Listener) { |
||||
|
listeners.push(listener); |
||||
|
|
||||
|
return function unsubscribe() { |
||||
|
const index = listeners.indexOf(listener); |
||||
|
if (index > -1) { |
||||
|
listeners.splice(index, 1); |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
@ -0,0 +1,25 @@ |
|||||
|
import { isBrowser } from './isBrowser'; |
||||
|
import { isDocumentVisible } from './isDocumentVisible'; |
||||
|
|
||||
|
type Listener = () => void; |
||||
|
|
||||
|
const listeners: Listener[] = []; |
||||
|
|
||||
|
if (isBrowser) { |
||||
|
const revalidate = () => { |
||||
|
if (!isDocumentVisible()) return; |
||||
|
for (let i = 0; i < listeners.length; i++) { |
||||
|
const listener = listeners[i]; |
||||
|
listener(); |
||||
|
} |
||||
|
}; |
||||
|
window.addEventListener('visibilitychange', revalidate, false); |
||||
|
} |
||||
|
|
||||
|
export default function subscribe(listener: Listener) { |
||||
|
listeners.push(listener); |
||||
|
return function unsubscribe() { |
||||
|
const index = listeners.indexOf(listener); |
||||
|
listeners.splice(index, 1); |
||||
|
}; |
||||
|
} |
||||
@ -0,0 +1,79 @@ |
|||||
|
import type { AppRouteModule } from '@/router/types'; |
||||
|
|
||||
|
import { LAYOUT } from '@/router/constant'; |
||||
|
|
||||
|
const charts: AppRouteModule = { |
||||
|
path: '/useRequest', |
||||
|
name: 'useRequest', |
||||
|
component: LAYOUT, |
||||
|
redirect: '/useRequest/base', |
||||
|
meta: { |
||||
|
orderNo: 900, |
||||
|
icon: 'ant-design:api-outlined', |
||||
|
title: 'useRequest', |
||||
|
}, |
||||
|
children: [ |
||||
|
{ |
||||
|
path: 'base', |
||||
|
name: 'useRequest-base', |
||||
|
meta: { title: '基础用法' }, |
||||
|
component: () => import('@/views/hooks/request/base'), |
||||
|
}, |
||||
|
{ |
||||
|
path: 'loading-delay', |
||||
|
name: 'useRequest-loading-delay', |
||||
|
meta: { title: 'Loading Delay' }, |
||||
|
component: () => import('@/views/hooks/request/loading-delay'), |
||||
|
}, |
||||
|
{ |
||||
|
path: 'polling', |
||||
|
name: 'useRequest-polling', |
||||
|
meta: { title: '轮询' }, |
||||
|
component: () => import('@/views/hooks/request/polling'), |
||||
|
}, |
||||
|
{ |
||||
|
path: 'ready', |
||||
|
name: 'useRequest-ready', |
||||
|
meta: { title: 'Ready' }, |
||||
|
component: () => import('@/views/hooks/request/ready'), |
||||
|
}, |
||||
|
{ |
||||
|
path: 'refresy-deps', |
||||
|
name: 'useRequest-refresy-deps', |
||||
|
meta: { title: '依赖刷新' }, |
||||
|
component: () => import('@/views/hooks/request/refresy-deps'), |
||||
|
}, |
||||
|
{ |
||||
|
path: 'refresh-on-window-focus', |
||||
|
name: 'useRequest-refresh-on-window-focus', |
||||
|
meta: { title: '屏幕聚焦重新请求' }, |
||||
|
component: () => import('@/views/hooks/request/refresh-on-window-focus'), |
||||
|
}, |
||||
|
{ |
||||
|
path: 'debounce', |
||||
|
name: 'useRequest-debounce', |
||||
|
meta: { title: '防抖' }, |
||||
|
component: () => import('@/views/hooks/request/debounce'), |
||||
|
}, |
||||
|
{ |
||||
|
path: 'throttle', |
||||
|
name: 'useRequest-throttle', |
||||
|
meta: { title: '节流' }, |
||||
|
component: () => import('@/views/hooks/request/throttle'), |
||||
|
}, |
||||
|
{ |
||||
|
path: 'cache', |
||||
|
name: 'useRequest-cache', |
||||
|
meta: { title: '缓存&SWR' }, |
||||
|
component: () => import('@/views/hooks/request/cache'), |
||||
|
}, |
||||
|
{ |
||||
|
path: 'retry', |
||||
|
name: 'useRequest-retry', |
||||
|
meta: { title: '错误重试' }, |
||||
|
component: () => import('@/views/hooks/request/retry'), |
||||
|
}, |
||||
|
], |
||||
|
}; |
||||
|
|
||||
|
export default charts; |
||||
@ -0,0 +1,328 @@ |
|||||
|
import { defineComponent, onMounted, ref, unref } from 'vue'; |
||||
|
import { Card, Spin, Typography, message, Input, Button, Space } from 'ant-design-vue'; |
||||
|
import { imitateApi } from './mock-api'; |
||||
|
import { useRequest } from '@vben/hooks'; |
||||
|
import { PageWrapper } from '@/components/Page'; |
||||
|
|
||||
|
const Demo1 = defineComponent({ |
||||
|
setup() { |
||||
|
const { data, error, loading } = useRequest(imitateApi); |
||||
|
|
||||
|
return () => ( |
||||
|
<Card title="默认用法"> |
||||
|
<Typography> |
||||
|
<Typography.Paragraph> |
||||
|
<Typography.Text type="danger">useRequest </Typography.Text> |
||||
|
的第一个参数是一个异步函数,在组件初次加载时,会自动触发该函数执行。同时自动管理该异步函数的 |
||||
|
<Typography.Text code>loading</Typography.Text> |
||||
|
<Typography.Text code>data</Typography.Text> |
||||
|
<Typography.Text code>error</Typography.Text> |
||||
|
等状态。 |
||||
|
</Typography.Paragraph> |
||||
|
<Typography.Text code> |
||||
|
{`const { data, error, loading } = useRequest(imitateApi);`} |
||||
|
</Typography.Text> |
||||
|
</Typography> |
||||
|
|
||||
|
{/* 基础案例 */} |
||||
|
<Spin spinning={loading.value}> |
||||
|
<div class="mt-4">{error.value ? 'failed to load' : `Username: ${data.value}`}</div> |
||||
|
</Spin> |
||||
|
</Card> |
||||
|
); |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
const Demo2 = defineComponent({ |
||||
|
setup() { |
||||
|
const search = ref(''); |
||||
|
const setSearch = (value: string) => { |
||||
|
search.value = value; |
||||
|
}; |
||||
|
|
||||
|
const { loading, run } = useRequest(imitateApi, { |
||||
|
manual: true, |
||||
|
onSuccess: (result, params) => { |
||||
|
if (result) { |
||||
|
setSearch(''); |
||||
|
message.success(`The username was changed to "${params[0]}" !`); |
||||
|
} |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
return () => ( |
||||
|
<Card title="手动触发" class="mt-2"> |
||||
|
<Typography> |
||||
|
<Typography.Paragraph> |
||||
|
如果设置了 |
||||
|
<Typography.Text type="danger"> options.manual = true </Typography.Text> |
||||
|
,则 useRequest 不会默认执行,需要通过 |
||||
|
<Typography.Text type="danger"> run </Typography.Text>来触发执行。 |
||||
|
</Typography.Paragraph> |
||||
|
<Typography.Text code> |
||||
|
{`const { loading, run } = useRequest(imitateApi, { manual: true });`} |
||||
|
</Typography.Text> |
||||
|
</Typography> |
||||
|
|
||||
|
{/* 手动触发 */} |
||||
|
<Space class="mt-4"> |
||||
|
<Input v-model={[search.value, 'value']} placeholder="Please enter username" /> |
||||
|
<Button type="primary" disabled={loading.value} onClick={() => run(search.value)}> |
||||
|
{loading.value ? 'Loading' : 'Edit'} |
||||
|
</Button> |
||||
|
</Space> |
||||
|
</Card> |
||||
|
); |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
const Demo3 = defineComponent({ |
||||
|
setup() { |
||||
|
const search = ref(''); |
||||
|
const setSearch = (value: string) => { |
||||
|
search.value = value; |
||||
|
}; |
||||
|
|
||||
|
const { loading, run } = useRequest(imitateApi, { |
||||
|
manual: true, |
||||
|
onBefore: (params) => { |
||||
|
message.info(`Start Request: ${params[0]}`); |
||||
|
}, |
||||
|
onSuccess: (result, params) => { |
||||
|
if (result) { |
||||
|
setSearch(''); |
||||
|
message.success(`The username was changed to "${params[0]}" !`); |
||||
|
} |
||||
|
}, |
||||
|
onError: (error) => { |
||||
|
message.error(error.message); |
||||
|
}, |
||||
|
onFinally: () => { |
||||
|
message.info(`Request finish`); |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
return () => ( |
||||
|
<Card title="生命周期" class="mt-2"> |
||||
|
<Typography> |
||||
|
<Typography.Paragraph> |
||||
|
<Typography.Text type="danger">useRequest </Typography.Text> |
||||
|
提供了以下几个生命周期配置项,供你在异步函数的不同阶段做一些处理。 |
||||
|
</Typography.Paragraph> |
||||
|
|
||||
|
<Typography.Paragraph> |
||||
|
<Typography.Text code>onBefore</Typography.Text> |
||||
|
请求之前触发 |
||||
|
</Typography.Paragraph> |
||||
|
|
||||
|
<Typography.Paragraph> |
||||
|
<Typography.Text code>onSuccess</Typography.Text> |
||||
|
请求成功触发 |
||||
|
</Typography.Paragraph> |
||||
|
|
||||
|
<Typography.Paragraph> |
||||
|
<Typography.Text code>onError</Typography.Text> |
||||
|
请求失败触发 |
||||
|
</Typography.Paragraph> |
||||
|
|
||||
|
<Typography.Paragraph> |
||||
|
<Typography.Text code>onFinally</Typography.Text> |
||||
|
请求完成触发 |
||||
|
</Typography.Paragraph> |
||||
|
</Typography> |
||||
|
|
||||
|
{/* 生命周期 */} |
||||
|
<Space> |
||||
|
<Input v-model={[search.value, 'value']} placeholder="Please enter username" /> |
||||
|
<Button type="primary" disabled={loading.value} onClick={() => run(search.value, true)}> |
||||
|
{loading.value ? 'Loading' : 'Edit'} |
||||
|
</Button> |
||||
|
<Button danger disabled={loading.value} onClick={() => run(search.value, false)}> |
||||
|
{loading.value ? 'Loading' : 'Error Edit'} |
||||
|
</Button> |
||||
|
</Space> |
||||
|
</Card> |
||||
|
); |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
const Demo4 = defineComponent({ |
||||
|
setup() { |
||||
|
const { data, loading, run, refresh } = useRequest(imitateApi, { |
||||
|
manual: true, |
||||
|
}); |
||||
|
|
||||
|
onMounted(() => run('lutz')); |
||||
|
|
||||
|
const changeData = () => { |
||||
|
data.value = `${Date.now()}`; |
||||
|
}; |
||||
|
|
||||
|
return () => ( |
||||
|
<Card title="刷新(重复上一次请求)" class="mt-2"> |
||||
|
<Typography> |
||||
|
<Typography.Paragraph> |
||||
|
<Typography.Text type="danger">useRequest </Typography.Text> |
||||
|
提供了 |
||||
|
<Typography.Text type="danger"> refresh </Typography.Text>和 |
||||
|
<Typography.Text type="danger"> refreshAsync </Typography.Text> |
||||
|
方法,使我们可以使用上一次的参数,重新发起请求。 |
||||
|
</Typography.Paragraph> |
||||
|
</Typography> |
||||
|
|
||||
|
<Spin spinning={loading.value}> |
||||
|
<Space> |
||||
|
<div>Username: {data.value}</div> |
||||
|
<Button type="primary" onClick={changeData}> |
||||
|
Change data |
||||
|
</Button> |
||||
|
<Button onClick={refresh}>Refresh</Button> |
||||
|
</Space> |
||||
|
</Spin> |
||||
|
</Card> |
||||
|
); |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
const Demo5 = defineComponent({ |
||||
|
setup() { |
||||
|
const search = ref(''); |
||||
|
const setSearch = (value: string) => { |
||||
|
search.value = value; |
||||
|
}; |
||||
|
|
||||
|
const { loading, run, cancel } = useRequest(imitateApi, { |
||||
|
manual: true, |
||||
|
onSuccess: (result, params) => { |
||||
|
if (result) { |
||||
|
setSearch(''); |
||||
|
message.success(`The username was changed to "${params[0]}" !`); |
||||
|
} |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
return () => ( |
||||
|
<Card title="取消响应" class="mt-2"> |
||||
|
<Typography> |
||||
|
<Typography.Paragraph> |
||||
|
<Typography.Text type="danger"> useRequest </Typography.Text>提供了 |
||||
|
<Typography.Text type="danger"> cancel </Typography.Text>函数,用于忽略当前 promise |
||||
|
返回的数据和错误 |
||||
|
</Typography.Paragraph> |
||||
|
</Typography> |
||||
|
|
||||
|
{/* 取消响应 */} |
||||
|
<Space> |
||||
|
<Input v-model={[search.value, 'value']} placeholder="Please enter username" /> |
||||
|
<Button type="primary" disabled={loading.value} onClick={() => run(search.value)}> |
||||
|
Edit |
||||
|
</Button> |
||||
|
<Button type="dashed" disabled={!loading.value} onClick={cancel}> |
||||
|
Cancel |
||||
|
</Button> |
||||
|
</Space> |
||||
|
</Card> |
||||
|
); |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
const Demo6 = defineComponent({ |
||||
|
setup() { |
||||
|
const search = ref(''); |
||||
|
|
||||
|
const { |
||||
|
data: username, |
||||
|
loading, |
||||
|
run, |
||||
|
params, |
||||
|
} = useRequest(imitateApi, { |
||||
|
defaultParams: ['lutz'], |
||||
|
}); |
||||
|
|
||||
|
const onChange = () => { |
||||
|
run(search.value); |
||||
|
}; |
||||
|
|
||||
|
return () => ( |
||||
|
<Card title="管理参数" class="mt-2"> |
||||
|
<Typography> |
||||
|
<Typography.Paragraph> |
||||
|
<Typography.Text type="danger"> useRequest </Typography.Text>返回的 |
||||
|
<Typography.Text type="danger"> params </Typography.Text>会记录当次调用 |
||||
|
<Typography.Text type="danger"> service </Typography.Text>的参数数组。比如你触发了 |
||||
|
<Typography.Text code>run(1, 2, 3)</Typography.Text>,则 |
||||
|
<Typography.Text type="danger"> params </Typography.Text> 等于 |
||||
|
<Typography.Text code> [1, 2, 3] </Typography.Text> |
||||
|
</Typography.Paragraph> |
||||
|
<Typography.Paragraph> |
||||
|
如果我们设置了 |
||||
|
<Typography.Text type="danger"> options.manual = false </Typography.Text>,则首次调用 |
||||
|
<Typography.Text type="danger"> service </Typography.Text> |
||||
|
的参数可以通过<Typography.Text type="danger"> options.defaultParams </Typography.Text> |
||||
|
来设置。 |
||||
|
</Typography.Paragraph> |
||||
|
</Typography> |
||||
|
|
||||
|
{/* 管理参数 */} |
||||
|
<Space> |
||||
|
<Input v-model={[search.value, 'value']} placeholder="Please enter username" /> |
||||
|
<Button disabled={loading.value} onClick={onChange}> |
||||
|
{loading.value ? 'Loading' : 'Edit'} |
||||
|
</Button> |
||||
|
</Space> |
||||
|
<div> |
||||
|
<div>UserId: {unref(params)?.[0]}</div> |
||||
|
<div>Username: {unref(username)}</div> |
||||
|
</div> |
||||
|
</Card> |
||||
|
); |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
export default defineComponent({ |
||||
|
setup() { |
||||
|
return () => ( |
||||
|
<PageWrapper |
||||
|
v-slots={{ |
||||
|
headerContent: () => ( |
||||
|
<Typography> |
||||
|
<Typography.Link |
||||
|
href="https://ahooks.js.org/zh-CN/hooks/use-request/index" |
||||
|
target="_blank" |
||||
|
> |
||||
|
ahooks{' '} |
||||
|
</Typography.Link> |
||||
|
useRequest 的 vue 版本,是一个强大的异步数据管理的 Hooks。 |
||||
|
<Typography.Paragraph> |
||||
|
<ul> |
||||
|
{[ |
||||
|
'自动请求/手动请求', |
||||
|
'轮询', |
||||
|
'防抖', |
||||
|
'节流', |
||||
|
'屏幕聚焦重新请求', |
||||
|
'错误重试', |
||||
|
'loading delay', |
||||
|
'SWR(stale-while-revalidate)', |
||||
|
'缓存', |
||||
|
].map((item) => ( |
||||
|
<li> |
||||
|
<Typography.Text>{item}</Typography.Text> |
||||
|
</li> |
||||
|
))} |
||||
|
</ul> |
||||
|
</Typography.Paragraph> |
||||
|
</Typography> |
||||
|
), |
||||
|
}} |
||||
|
> |
||||
|
<Demo1 /> |
||||
|
<Demo2 /> |
||||
|
<Demo3 /> |
||||
|
<Demo4 /> |
||||
|
<Demo5 /> |
||||
|
<Demo6 /> |
||||
|
</PageWrapper> |
||||
|
); |
||||
|
}, |
||||
|
}); |
||||
@ -0,0 +1,318 @@ |
|||||
|
import { defineComponent, ref, unref } from 'vue'; |
||||
|
import { Card, Typography, Button, Input, Space, message } from 'ant-design-vue'; |
||||
|
import { getArticle } from './mock-api'; |
||||
|
import { useRequest, clearCache } from '@vben/hooks'; |
||||
|
import { PageWrapper } from '@/components/Page'; |
||||
|
|
||||
|
const Article1 = defineComponent({ |
||||
|
props: { |
||||
|
cacheKey: { |
||||
|
type: String, |
||||
|
default: 'cacheKey-demo', |
||||
|
}, |
||||
|
}, |
||||
|
setup(props) { |
||||
|
const { loading, data } = useRequest(getArticle, { |
||||
|
cacheKey: props.cacheKey, |
||||
|
}); |
||||
|
|
||||
|
return () => ( |
||||
|
<> |
||||
|
<p>Background loading: {loading.value ? 'true' : 'false'}</p> |
||||
|
<p>Latest request time: {unref(data)?.time}</p> |
||||
|
<p>{unref(data)?.data}</p> |
||||
|
</> |
||||
|
); |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
const Demo1 = defineComponent({ |
||||
|
setup() { |
||||
|
const state = ref(false); |
||||
|
const toggle = (bool?: boolean) => { |
||||
|
state.value = bool ?? !state.value; |
||||
|
}; |
||||
|
|
||||
|
return () => ( |
||||
|
<Card title="SWR"> |
||||
|
<Typography> |
||||
|
<Typography.Paragraph> |
||||
|
下面的示例,我们设置了 |
||||
|
<Typography.Text type="danger"> cacheKey </Typography.Text> |
||||
|
,在组件第二次加载时,会优先返回缓存的内容,然后在背后重新发起请求。你可以通过点击按钮来体验效果。 |
||||
|
</Typography.Paragraph> |
||||
|
</Typography> |
||||
|
|
||||
|
{/* SWR */} |
||||
|
<div class="mt-4"> |
||||
|
<Button type="primary" onClick={() => toggle()}> |
||||
|
{state.value ? 'hidden' : 'show'} |
||||
|
</Button> |
||||
|
{state.value && <Article1 />} |
||||
|
</div> |
||||
|
</Card> |
||||
|
); |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
const Article2 = defineComponent({ |
||||
|
setup() { |
||||
|
const { loading, data } = useRequest(getArticle, { |
||||
|
cacheKey: 'staleTime-demo', |
||||
|
staleTime: 5000, |
||||
|
}); |
||||
|
|
||||
|
return () => ( |
||||
|
<> |
||||
|
<p>Background loading: {loading.value ? 'true' : 'false'}</p> |
||||
|
<p>Latest request time: {unref(data)?.time}</p> |
||||
|
<p>{unref(data)?.data}</p> |
||||
|
</> |
||||
|
); |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
const Demo2 = defineComponent({ |
||||
|
setup() { |
||||
|
const state = ref(false); |
||||
|
const toggle = (bool?: boolean) => { |
||||
|
state.value = bool ?? !state.value; |
||||
|
}; |
||||
|
|
||||
|
return () => ( |
||||
|
<Card title="数据保持新鲜" class="mt-2"> |
||||
|
<Typography> |
||||
|
<Typography.Paragraph> |
||||
|
通过设置 |
||||
|
<Typography.Text type="danger"> staleTime </Typography.Text> |
||||
|
,我们可以指定数据新鲜时间,在这个时间内,不会重新发起请求。下面的示例设置了 5s |
||||
|
的新鲜时间,你可以通过点击按钮来体验效果 |
||||
|
</Typography.Paragraph> |
||||
|
</Typography> |
||||
|
|
||||
|
{/* 数据保持新鲜 */} |
||||
|
<div class="mt-4"> |
||||
|
<Button type="primary" onClick={() => toggle()}> |
||||
|
{state.value ? 'hidden' : 'show'} |
||||
|
</Button> |
||||
|
{state.value && <Article2 />} |
||||
|
</div> |
||||
|
</Card> |
||||
|
); |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
const Article3 = defineComponent({ |
||||
|
setup() { |
||||
|
const { loading, data, refresh } = useRequest(getArticle, { |
||||
|
cacheKey: 'cacheKey-share', |
||||
|
}); |
||||
|
|
||||
|
return () => ( |
||||
|
<> |
||||
|
<p>Background loading: {loading.value ? 'true' : 'false'}</p> |
||||
|
<Button type="primary" onClick={refresh}> |
||||
|
更新 |
||||
|
</Button> |
||||
|
<p>Latest request time: {unref(data)?.time}</p> |
||||
|
<p>{unref(data)?.data}</p> |
||||
|
</> |
||||
|
); |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
const Demo3 = defineComponent({ |
||||
|
setup() { |
||||
|
return () => ( |
||||
|
<Card title="数据共享" class="mt-2"> |
||||
|
<Typography> |
||||
|
<Typography.Paragraph> |
||||
|
同一个<Typography.Text type="danger"> cacheKey </Typography.Text> |
||||
|
的内容,在全局是共享的,这会带来以下几个特性 |
||||
|
</Typography.Paragraph> |
||||
|
|
||||
|
<Typography.Paragraph> |
||||
|
<ul> |
||||
|
<li> |
||||
|
请求 Promise 共享,相同的<Typography.Text type="danger"> cacheKey </Typography.Text> |
||||
|
同时只会有一个在发起请求,后发起的会共用同一个请求 Promise |
||||
|
</li> |
||||
|
<li> |
||||
|
数据同步,任何时候,当我们改变其中某个 cacheKey 的内容时,其它相同 |
||||
|
<Typography.Text type="danger"> cacheKey </Typography.Text> |
||||
|
的内容均会同步 |
||||
|
</li> |
||||
|
</ul> |
||||
|
</Typography.Paragraph> |
||||
|
</Typography> |
||||
|
|
||||
|
{/* 数据共享 */} |
||||
|
<div class="mt-4"> |
||||
|
<h2>Article 1</h2> |
||||
|
<Article3 /> |
||||
|
<h2>Article 2</h2> |
||||
|
<Article3 /> |
||||
|
</div> |
||||
|
</Card> |
||||
|
); |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
const Article4 = defineComponent({ |
||||
|
setup() { |
||||
|
const { loading, data, params, run } = useRequest(getArticle, { |
||||
|
cacheKey: 'cacheKey-share4', |
||||
|
}); |
||||
|
|
||||
|
const keyword = ref(params.value?.[0] || ''); |
||||
|
|
||||
|
return () => ( |
||||
|
<> |
||||
|
<Space> |
||||
|
<Input v-model={[keyword.value, 'value']} /> |
||||
|
<Button onClick={() => run(keyword.value)}>Get</Button> |
||||
|
</Space> |
||||
|
<p>Background loading: {loading.value ? 'true' : 'false'}</p> |
||||
|
<p>Latest request time: {unref(data)?.time}</p> |
||||
|
<p>Latest request data: {unref(data)?.data}</p> |
||||
|
<p>keyword: {keyword.value}</p> |
||||
|
</> |
||||
|
); |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
const Demo4 = defineComponent({ |
||||
|
setup() { |
||||
|
const state = ref(false); |
||||
|
const toggle = (bool?: boolean) => { |
||||
|
state.value = bool ?? !state.value; |
||||
|
}; |
||||
|
|
||||
|
return () => ( |
||||
|
<Card title="参数缓存" class="mt-2"> |
||||
|
<Typography> |
||||
|
<Typography.Paragraph> |
||||
|
缓存的数据包括 data 和 params,通过 params |
||||
|
缓存机制,我们可以记忆上一次请求的条件,并在下次初始化 |
||||
|
</Typography.Paragraph> |
||||
|
</Typography> |
||||
|
|
||||
|
{/* 参数缓存 */} |
||||
|
<div class="mt-4"> |
||||
|
<Button type="primary" onClick={() => toggle()}> |
||||
|
{state.value ? 'hidden' : 'show'} |
||||
|
</Button> |
||||
|
<div class="mt-2">{state.value && <Article4 />}</div> |
||||
|
</div> |
||||
|
</Card> |
||||
|
); |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
const Demo5 = defineComponent({ |
||||
|
setup() { |
||||
|
const state = ref(false); |
||||
|
const toggle = (bool?: boolean) => { |
||||
|
state.value = bool ?? !state.value; |
||||
|
}; |
||||
|
|
||||
|
const clear = (cacheKey?: string | string[]) => { |
||||
|
clearCache(cacheKey); |
||||
|
const tips = Array.isArray(cacheKey) ? cacheKey.join('、') : cacheKey; |
||||
|
message.success(`Clear ${tips ?? 'All'} finished`); |
||||
|
}; |
||||
|
|
||||
|
return () => ( |
||||
|
<Card title="删除缓存" class="mt-2"> |
||||
|
<Typography> |
||||
|
<Typography.Paragraph> |
||||
|
useRequest 提供了一个 clearCache 方法,可以清除指定 cacheKey 的缓存数据。 |
||||
|
</Typography.Paragraph> |
||||
|
</Typography> |
||||
|
|
||||
|
{/* 删除缓存 */} |
||||
|
<div class="mt-4"> |
||||
|
<Space> |
||||
|
<Button type="primary" onClick={() => toggle()}> |
||||
|
{state.value ? 'hidden' : 'show'} |
||||
|
</Button> |
||||
|
<Button onClick={() => clear('Article1')}>Clear Article1</Button> |
||||
|
<Button onClick={() => clear('Article2')}>Clear Article2</Button> |
||||
|
<Button onClick={() => clear(['Article2', 'Article3'])}> |
||||
|
Clear Article2 and Article3 |
||||
|
</Button> |
||||
|
<Button onClick={() => clear()}>Clear All</Button> |
||||
|
</Space> |
||||
|
<h2>Article 1</h2> |
||||
|
{state.value && <Article1 cacheKey="Article1" />} |
||||
|
<h2>Article 2</h2> |
||||
|
{state.value && <Article1 cacheKey="Article2" />} |
||||
|
<h2>Article 3</h2> |
||||
|
{state.value && <Article1 cacheKey="Article3" />} |
||||
|
</div> |
||||
|
</Card> |
||||
|
); |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
const Article6 = defineComponent({ |
||||
|
setup() { |
||||
|
const cacheKey = 'setCache-demo6'; |
||||
|
const { loading, data } = useRequest(getArticle, { |
||||
|
cacheKey, |
||||
|
setCache: (data) => localStorage.setItem(cacheKey, JSON.stringify(data)), |
||||
|
getCache: () => JSON.parse(localStorage.getItem(cacheKey) || '{}'), |
||||
|
}); |
||||
|
|
||||
|
return () => ( |
||||
|
<> |
||||
|
<p>Background loading: {loading.value ? 'true' : 'false'}</p> |
||||
|
<p>Latest request time: {unref(data)?.time}</p> |
||||
|
<p>{unref(data)?.data}</p> |
||||
|
</> |
||||
|
); |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
const Demo6 = defineComponent({ |
||||
|
setup() { |
||||
|
const state = ref(false); |
||||
|
const toggle = (bool?: boolean) => { |
||||
|
state.value = bool ?? !state.value; |
||||
|
}; |
||||
|
|
||||
|
return () => ( |
||||
|
<Card title="自定义缓存" class="mt-2"> |
||||
|
<Typography> |
||||
|
<Typography.Paragraph> |
||||
|
通过配置 setCache 和 getCache,可以自定义数据缓存,比如可以将数据存储到 |
||||
|
localStorage、IndexDB 等。 |
||||
|
</Typography.Paragraph> |
||||
|
</Typography> |
||||
|
|
||||
|
{/* 自定义缓存 */} |
||||
|
<div class="mt-4"> |
||||
|
<Button type="primary" onClick={() => toggle()}> |
||||
|
{state.value ? 'hidden' : 'show'} |
||||
|
</Button> |
||||
|
<div class="mt-2">{state.value && <Article6 />}</div> |
||||
|
</div> |
||||
|
</Card> |
||||
|
); |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
export default defineComponent({ |
||||
|
setup() { |
||||
|
return () => ( |
||||
|
<PageWrapper> |
||||
|
<Demo1 /> |
||||
|
<Demo2 /> |
||||
|
<Demo3 /> |
||||
|
<Demo4 /> |
||||
|
<Demo5 /> |
||||
|
<Demo6 /> |
||||
|
</PageWrapper> |
||||
|
); |
||||
|
}, |
||||
|
}); |
||||
@ -0,0 +1,62 @@ |
|||||
|
import { defineComponent, ref } from 'vue'; |
||||
|
import { Card, Typography, Input, Spin, Space } from 'ant-design-vue'; |
||||
|
import { imitateApi } from './mock-api'; |
||||
|
import { useRequest } from '@vben/hooks'; |
||||
|
import { PageWrapper } from '@/components/Page'; |
||||
|
|
||||
|
const Demo1 = defineComponent({ |
||||
|
setup() { |
||||
|
const search = ref(''); |
||||
|
|
||||
|
const { data, loading } = useRequest(imitateApi, { |
||||
|
debounceWait: 1000, |
||||
|
refreshDeps: [search], |
||||
|
}); |
||||
|
|
||||
|
return () => ( |
||||
|
<Card title="防抖"> |
||||
|
<Typography> |
||||
|
<Typography.Paragraph> |
||||
|
通过设置<Typography.Text type="danger"> options.debounceWait </Typography.Text> |
||||
|
,进入防抖模式,此时如果频繁触发 |
||||
|
<Typography.Text code> run </Typography.Text> |
||||
|
或者 |
||||
|
<Typography.Text code> runAsync </Typography.Text> |
||||
|
则会以防抖策略进行请求。 |
||||
|
</Typography.Paragraph> |
||||
|
|
||||
|
<Typography.Paragraph> |
||||
|
<Typography.Text code> |
||||
|
{`const { data, run } = useRequest(imitateApi, { debounceWait: 300, manual: true });`} |
||||
|
</Typography.Text> |
||||
|
</Typography.Paragraph> |
||||
|
|
||||
|
<Typography.Paragraph> |
||||
|
如上示例代码,频繁触发 |
||||
|
<Typography.Text code> run </Typography.Text>, 300ms 执行一次。 |
||||
|
</Typography.Paragraph> |
||||
|
|
||||
|
<Typography.Paragraph>你可以在下面 input 框中快速输入文本,体验效果</Typography.Paragraph> |
||||
|
</Typography> |
||||
|
|
||||
|
{/* 防抖 */} |
||||
|
<Spin spinning={loading.value}> |
||||
|
<Space direction="vertical"> |
||||
|
<Input v-model={[search.value, 'value']} placeholder="Please enter username" /> |
||||
|
<div>Username: {data.value}</div> |
||||
|
</Space> |
||||
|
</Spin> |
||||
|
</Card> |
||||
|
); |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
export default defineComponent({ |
||||
|
setup() { |
||||
|
return () => ( |
||||
|
<PageWrapper> |
||||
|
<Demo1 /> |
||||
|
</PageWrapper> |
||||
|
); |
||||
|
}, |
||||
|
}); |
||||
@ -0,0 +1,61 @@ |
|||||
|
import { defineComponent, unref } from 'vue'; |
||||
|
import { Card, Typography, Button, Space } from 'ant-design-vue'; |
||||
|
import { useRequest } from '@vben/hooks'; |
||||
|
import { PageWrapper } from '@/components/Page'; |
||||
|
import { imitateApi } from './mock-api'; |
||||
|
|
||||
|
export default defineComponent({ |
||||
|
setup() { |
||||
|
const action = useRequest(imitateApi); |
||||
|
|
||||
|
const withLoadingDelayAction = useRequest(imitateApi, { |
||||
|
loadingDelay: 300, |
||||
|
}); |
||||
|
|
||||
|
const trigger = () => { |
||||
|
action.run('lutz'); |
||||
|
withLoadingDelayAction.run('lutz'); |
||||
|
}; |
||||
|
|
||||
|
return () => ( |
||||
|
<PageWrapper> |
||||
|
<Card title="Loading Delay"> |
||||
|
<Typography> |
||||
|
<Typography.Paragraph> |
||||
|
通过设置 |
||||
|
<Typography.Text type="danger"> options.loadingDelay </Typography.Text> |
||||
|
可以延迟 <Typography.Text code>loading</Typography.Text> 变成 |
||||
|
<Typography.Text code>true</Typography.Text> |
||||
|
的时间,有效防止闪烁。 |
||||
|
</Typography.Paragraph> |
||||
|
|
||||
|
<Typography.Paragraph> |
||||
|
<Typography.Text code> |
||||
|
{`const { loading, data } = useRequest(imitateApi, { loadingDelay: 300 });`} |
||||
|
</Typography.Text> |
||||
|
</Typography.Paragraph> |
||||
|
|
||||
|
<Typography.Paragraph> |
||||
|
例如上面的场景,假如 imitateApi 在 300ms 内返回,则{' '} |
||||
|
<Typography.Text code>loading</Typography.Text> 不会变成{' '} |
||||
|
<Typography.Text code>true</Typography.Text> Loading... 的情况。 |
||||
|
</Typography.Paragraph> |
||||
|
</Typography> |
||||
|
|
||||
|
<Space direction="vertical"> |
||||
|
<Button onClick={trigger}>Run</Button> |
||||
|
|
||||
|
<div>Username: {unref(action.loading) ? 'Loading...' : unref(action.data)}</div> |
||||
|
|
||||
|
<div> |
||||
|
Username:{' '} |
||||
|
{unref(withLoadingDelayAction.loading) |
||||
|
? 'Loading...' |
||||
|
: unref(withLoadingDelayAction.data)} |
||||
|
</div> |
||||
|
</Space> |
||||
|
</Card> |
||||
|
</PageWrapper> |
||||
|
); |
||||
|
}, |
||||
|
}); |
||||
@ -0,0 +1,27 @@ |
|||||
|
import Mock from 'mockjs'; |
||||
|
|
||||
|
export async function imitateApi(username?: string, pass: boolean = true): Promise<string> { |
||||
|
return new Promise((resolve, reject) => { |
||||
|
setTimeout(() => { |
||||
|
if (pass) { |
||||
|
resolve(username ?? Mock.mock('@name')); |
||||
|
} else { |
||||
|
reject(new Error(`Failed to modify username: ${username}`)); |
||||
|
} |
||||
|
}, 1250); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
export async function getArticle( |
||||
|
keyword?: string, |
||||
|
): Promise<{ data: string; time: number; keyword?: string }> { |
||||
|
return new Promise((resolve) => { |
||||
|
setTimeout(() => { |
||||
|
resolve({ |
||||
|
data: Mock.mock('@paragraph'), |
||||
|
time: new Date().getTime(), |
||||
|
keyword, |
||||
|
}); |
||||
|
}, 1000); |
||||
|
}); |
||||
|
} |
||||
@ -0,0 +1,96 @@ |
|||||
|
import { defineComponent } from 'vue'; |
||||
|
import { Card, Typography, Button, Space, message } from 'ant-design-vue'; |
||||
|
import { imitateApi } from './mock-api'; |
||||
|
import { useRequest } from '@vben/hooks'; |
||||
|
import { PageWrapper } from '@/components/Page'; |
||||
|
|
||||
|
const Demo1 = defineComponent({ |
||||
|
setup() { |
||||
|
const { data, loading, run, cancel } = useRequest(imitateApi, { |
||||
|
pollingInterval: 1000, |
||||
|
pollingWhenHidden: false, |
||||
|
// onSuccess() {
|
||||
|
// console.log('不可见是否运行呢'); // 测试不可见时,是否还在执行
|
||||
|
// },
|
||||
|
}); |
||||
|
|
||||
|
return () => ( |
||||
|
<Card title="默认用法"> |
||||
|
<Typography> |
||||
|
<Typography.Paragraph> |
||||
|
通过设置 |
||||
|
<Typography.Text type="danger"> options.pollingInterval </Typography.Text> |
||||
|
,进入轮询模式,useRequest 会定时触发 service 执行。 |
||||
|
</Typography.Paragraph> |
||||
|
<Typography.Paragraph> |
||||
|
<Typography.Text code> |
||||
|
{`const { data, run, cancel } = useRequest(imitateApi, { pollingInterval: 3000 });`} |
||||
|
</Typography.Text> |
||||
|
</Typography.Paragraph> |
||||
|
</Typography> |
||||
|
|
||||
|
<div> |
||||
|
<div>Username: {loading.value ? 'Loading' : data.value}</div> |
||||
|
<Space> |
||||
|
<Button onClick={() => run()}>start</Button> |
||||
|
<Button type="dashed" onClick={cancel}> |
||||
|
stop |
||||
|
</Button> |
||||
|
</Space> |
||||
|
</div> |
||||
|
</Card> |
||||
|
); |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
const Demo2 = defineComponent({ |
||||
|
setup() { |
||||
|
const { data, loading, run, cancel } = useRequest(imitateApi, { |
||||
|
manual: true, |
||||
|
pollingInterval: 3000, |
||||
|
pollingErrorRetryCount: 3, |
||||
|
pollingWhenHidden: false, |
||||
|
onError: (error) => { |
||||
|
message.error(error.message); |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
return () => ( |
||||
|
<Card title="轮询错误重试" class="mt-2"> |
||||
|
<Typography> |
||||
|
<Typography.Paragraph> |
||||
|
通过 |
||||
|
<Typography.Text type="danger"> options.pollingErrorRetryCount </Typography.Text> |
||||
|
轮询错误重试次数。 |
||||
|
</Typography.Paragraph> |
||||
|
<Typography.Paragraph> |
||||
|
<Typography.Text code> |
||||
|
{`const { data, run, cancel } = useRequest(imitateApi, { pollingInterval: 3000, pollingErrorRetryCount: 3 });`} |
||||
|
</Typography.Text> |
||||
|
</Typography.Paragraph> |
||||
|
</Typography> |
||||
|
|
||||
|
<div> |
||||
|
<div>Username: {loading.value ? 'Loading' : data.value}</div> |
||||
|
<Space> |
||||
|
<Button onClick={() => run('lutz', false)}>start</Button> |
||||
|
<Button type="dashed" onClick={cancel}> |
||||
|
stop |
||||
|
</Button> |
||||
|
</Space> |
||||
|
</div> |
||||
|
</Card> |
||||
|
); |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
export default defineComponent({ |
||||
|
setup() { |
||||
|
return () => ( |
||||
|
<PageWrapper> |
||||
|
<Demo1 /> |
||||
|
<Demo2 /> |
||||
|
</PageWrapper> |
||||
|
); |
||||
|
}, |
||||
|
}); |
||||
@ -0,0 +1,86 @@ |
|||||
|
import { defineComponent, ref, unref } from 'vue'; |
||||
|
import { Card, Typography, Button, Space } from 'ant-design-vue'; |
||||
|
import { imitateApi } from './mock-api'; |
||||
|
import { useRequest } from '@vben/hooks'; |
||||
|
import { PageWrapper } from '@/components/Page'; |
||||
|
|
||||
|
const Demo1 = defineComponent({ |
||||
|
setup() { |
||||
|
const ready = ref(false); |
||||
|
const toggle = (bool?: boolean) => { |
||||
|
ready.value = bool ?? !ready.value; |
||||
|
}; |
||||
|
const { data, loading } = useRequest(imitateApi, { ready }); |
||||
|
|
||||
|
return () => ( |
||||
|
<Card title="自动模式"> |
||||
|
<Typography> |
||||
|
<Typography.Paragraph> |
||||
|
以下示例演示了自动模式下 |
||||
|
<Typography.Text type="danger"> ready </Typography.Text> 的行为。每次 |
||||
|
<Typography.Text type="danger"> ready </Typography.Text> 从 false 变为 true |
||||
|
时,都会重新发起请求。 |
||||
|
</Typography.Paragraph> |
||||
|
</Typography> |
||||
|
|
||||
|
<div> |
||||
|
<Space> |
||||
|
<div>Ready: {JSON.stringify(unref(ready))}</div> |
||||
|
<Button onClick={() => toggle()}>Toggle Ready</Button> |
||||
|
</Space> |
||||
|
<div>Username: {loading.value ? 'Loading' : unref(data)}</div> |
||||
|
</div> |
||||
|
</Card> |
||||
|
); |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
const Demo2 = defineComponent({ |
||||
|
setup() { |
||||
|
const ready = ref(false); |
||||
|
const toggle = (bool?: boolean) => { |
||||
|
ready.value = bool ?? !ready.value; |
||||
|
}; |
||||
|
const { data, loading, run } = useRequest(imitateApi, { manual: true, ready }); |
||||
|
|
||||
|
return () => ( |
||||
|
<Card title="手动模式" class="mt-2"> |
||||
|
<Typography> |
||||
|
<Typography.Paragraph> |
||||
|
以下示例演示了手动模式下 |
||||
|
<Typography.Text type="danger"> ready </Typography.Text> |
||||
|
的行为。只有当 |
||||
|
<Typography.Text type="danger"> ready </Typography.Text> |
||||
|
等于 true 时,run 才会执行。 |
||||
|
</Typography.Paragraph> |
||||
|
</Typography> |
||||
|
|
||||
|
<div> |
||||
|
<Space> |
||||
|
<div>Ready: {JSON.stringify(unref(ready))}</div> |
||||
|
<Button onClick={() => toggle()}>Toggle Ready</Button> |
||||
|
</Space> |
||||
|
<div class="mt-2"> |
||||
|
<Space> |
||||
|
<div>Username: {loading.value ? 'Loading' : unref(data)}</div> |
||||
|
<Button type="primary" disabled={!ready.value} onClick={() => run()}> |
||||
|
Run |
||||
|
</Button> |
||||
|
</Space> |
||||
|
</div> |
||||
|
</div> |
||||
|
</Card> |
||||
|
); |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
export default defineComponent({ |
||||
|
setup() { |
||||
|
return () => ( |
||||
|
<PageWrapper> |
||||
|
<Demo1 /> |
||||
|
<Demo2 /> |
||||
|
</PageWrapper> |
||||
|
); |
||||
|
}, |
||||
|
}); |
||||
@ -0,0 +1,50 @@ |
|||||
|
import { defineComponent } from 'vue'; |
||||
|
import { Card, Typography, Spin } from 'ant-design-vue'; |
||||
|
import { imitateApi } from './mock-api'; |
||||
|
import { useRequest } from '@vben/hooks'; |
||||
|
import { PageWrapper } from '@/components/Page'; |
||||
|
|
||||
|
const Demo1 = defineComponent({ |
||||
|
setup() { |
||||
|
const { data, loading } = useRequest(imitateApi, { |
||||
|
refreshOnWindowFocus: true, |
||||
|
}); |
||||
|
|
||||
|
return () => ( |
||||
|
<Card title="屏幕聚焦重新请求"> |
||||
|
<Typography> |
||||
|
<Typography.Paragraph> |
||||
|
通过设置<Typography.Text type="danger"> options.refreshOnWindowFocus </Typography.Text> |
||||
|
,在浏览器窗口 refocus 和 revisible 时, 会重新发起请求。 |
||||
|
</Typography.Paragraph> |
||||
|
|
||||
|
<Typography.Paragraph> |
||||
|
<Typography.Text code> |
||||
|
{`const { data, run } = useRequest(imitateApi, { refreshOnWindowFocus: true });`} |
||||
|
</Typography.Text> |
||||
|
</Typography.Paragraph> |
||||
|
|
||||
|
<Typography.Paragraph> |
||||
|
你可以点击浏览器外部,再点击当前页面来体验效果(或者隐藏当前页面,重新展示),如果和上一次请求间隔大于 |
||||
|
5000ms, 则会重新请求一次。 |
||||
|
</Typography.Paragraph> |
||||
|
</Typography> |
||||
|
|
||||
|
{/* 屏幕聚焦重新请求 */} |
||||
|
<Spin spinning={loading.value}> |
||||
|
<div>Username: {data.value}</div> |
||||
|
</Spin> |
||||
|
</Card> |
||||
|
); |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
export default defineComponent({ |
||||
|
setup() { |
||||
|
return () => ( |
||||
|
<PageWrapper> |
||||
|
<Demo1 /> |
||||
|
</PageWrapper> |
||||
|
); |
||||
|
}, |
||||
|
}); |
||||
@ -0,0 +1,43 @@ |
|||||
|
import { defineComponent, ref, unref } from 'vue'; |
||||
|
import { Card, Typography, Select } from 'ant-design-vue'; |
||||
|
import { imitateApi } from './mock-api'; |
||||
|
import { useRequest } from '@vben/hooks'; |
||||
|
import { PageWrapper } from '@/components/Page'; |
||||
|
|
||||
|
const options = [ |
||||
|
{ label: 'Jack', value: 'Jack' }, |
||||
|
{ label: 'Lucy', value: 'Lucy' }, |
||||
|
{ label: 'Lutz', value: 'Lutz' }, |
||||
|
]; |
||||
|
|
||||
|
const Demo1 = defineComponent({ |
||||
|
setup() { |
||||
|
const select = ref('Lutz'); |
||||
|
const { data, loading } = useRequest(() => imitateApi(select.value), { refreshDeps: [select] }); |
||||
|
|
||||
|
return () => ( |
||||
|
<Card title="依赖刷新"> |
||||
|
<Typography> |
||||
|
<Typography.Paragraph> |
||||
|
useRequest 提供了一个 |
||||
|
<Typography.Text type="danger"> options.refreshDeps </Typography.Text> |
||||
|
参数,当它的值变化后,会重新触发请求。 |
||||
|
</Typography.Paragraph> |
||||
|
</Typography> |
||||
|
|
||||
|
<Select v-model={[select.value, 'value']} options={options} style="width: 220px" /> |
||||
|
<p>Username: {loading.value ? 'Loading' : unref(data)}</p> |
||||
|
</Card> |
||||
|
); |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
export default defineComponent({ |
||||
|
setup() { |
||||
|
return () => ( |
||||
|
<PageWrapper> |
||||
|
<Demo1 /> |
||||
|
</PageWrapper> |
||||
|
); |
||||
|
}, |
||||
|
}); |
||||
@ -0,0 +1,53 @@ |
|||||
|
import { defineComponent, ref } from 'vue'; |
||||
|
import { Card, Typography, Input, Button, Space, message } from 'ant-design-vue'; |
||||
|
import { imitateApi } from './mock-api'; |
||||
|
import { useRequest } from '@vben/hooks'; |
||||
|
import { PageWrapper } from '@/components/Page'; |
||||
|
|
||||
|
const Demo1 = defineComponent({ |
||||
|
setup() { |
||||
|
let count = 0; |
||||
|
const search = ref(''); |
||||
|
|
||||
|
const { loading, run } = useRequest(imitateApi, { |
||||
|
manual: true, |
||||
|
retryCount: 3, |
||||
|
onError: (error) => { |
||||
|
message.error(error.message + ` count: ${count++}.`); |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
return () => ( |
||||
|
<Card title="错误重试"> |
||||
|
<Typography> |
||||
|
<Typography.Paragraph> |
||||
|
通过设置 |
||||
|
<Typography.Text type="danger"> options.retryCount </Typography.Text> |
||||
|
,指定错误重试次数,则 useRequest 在失败后会进行重试。 |
||||
|
</Typography.Paragraph> |
||||
|
<Typography.Text code> |
||||
|
{`const { data, run } = useRequest(imitateApi, { retryCount: 3 });`} |
||||
|
</Typography.Text> |
||||
|
</Typography> |
||||
|
|
||||
|
{/* 错误重试 */} |
||||
|
<Space class="mt-4"> |
||||
|
<Input v-model={[search.value, 'value']} placeholder="Please enter username" /> |
||||
|
<Button type="primary" disabled={loading.value} onClick={() => run(search.value, false)}> |
||||
|
{loading.value ? 'Loading' : 'Edit'} |
||||
|
</Button> |
||||
|
</Space> |
||||
|
</Card> |
||||
|
); |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
export default defineComponent({ |
||||
|
setup() { |
||||
|
return () => ( |
||||
|
<PageWrapper> |
||||
|
<Demo1 /> |
||||
|
</PageWrapper> |
||||
|
); |
||||
|
}, |
||||
|
}); |
||||
@ -0,0 +1,61 @@ |
|||||
|
import { defineComponent, ref } from 'vue'; |
||||
|
import { Card, Typography, Input, Spin, Space } from 'ant-design-vue'; |
||||
|
import { imitateApi } from './mock-api'; |
||||
|
import { useRequest } from '@vben/hooks'; |
||||
|
import { PageWrapper } from '@/components/Page'; |
||||
|
|
||||
|
const Demo1 = defineComponent({ |
||||
|
setup() { |
||||
|
const search = ref(''); |
||||
|
|
||||
|
const { data, loading } = useRequest(imitateApi, { |
||||
|
throttleWait: 1000, |
||||
|
refreshDeps: [search], |
||||
|
}); |
||||
|
|
||||
|
return () => ( |
||||
|
<Card title="节流"> |
||||
|
<Typography> |
||||
|
<Typography.Paragraph> |
||||
|
通过设置 |
||||
|
<Typography.Text type="danger"> options.throttleWait </Typography.Text> |
||||
|
,进入节流模式,此时如果频繁触发 |
||||
|
<Typography.Text code> run </Typography.Text>或者 |
||||
|
<Typography.Text code> runAsync </Typography.Text>, 则会以节流策略进行请求。 |
||||
|
</Typography.Paragraph> |
||||
|
|
||||
|
<Typography.Paragraph> |
||||
|
<Typography.Text code> |
||||
|
{`const { data, run } = useRequest(imitateApi, { throttleWait: 300, manual: true });`} |
||||
|
</Typography.Text> |
||||
|
</Typography.Paragraph> |
||||
|
|
||||
|
<Typography.Paragraph> |
||||
|
如上示例代码,频繁触发 |
||||
|
<Typography.Text code> run </Typography.Text>, 300ms 执行一次。 |
||||
|
</Typography.Paragraph> |
||||
|
|
||||
|
<Typography.Paragraph>你可以在下面 input 框中快速输入文本,体验效果</Typography.Paragraph> |
||||
|
</Typography> |
||||
|
|
||||
|
{/* 节流 */} |
||||
|
<Spin spinning={loading.value}> |
||||
|
<Space direction="vertical"> |
||||
|
<Input v-model={[search.value, 'value']} placeholder="Please enter username" /> |
||||
|
<div>Username: {data.value}</div> |
||||
|
</Space> |
||||
|
</Spin> |
||||
|
</Card> |
||||
|
); |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
|
export default defineComponent({ |
||||
|
setup() { |
||||
|
return () => ( |
||||
|
<PageWrapper> |
||||
|
<Demo1 /> |
||||
|
</PageWrapper> |
||||
|
); |
||||
|
}, |
||||
|
}); |
||||
Loading…
Reference in new issue