From d6d1120d00a24b7a97bd37f6a0786c1265fa870d Mon Sep 17 00:00:00 2001 From: luocong2016 Date: Fri, 22 Dec 2023 16:50:41 +0800 Subject: [PATCH] =?UTF-8?q?feat(demo):=20hooks=20useRequest=20=E5=BC=82?= =?UTF-8?q?=E6=AD=A5=E6=95=B0=E6=8D=AE=E7=AE=A1=E7=90=86=20(#3447)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/hooks/package.json | 1 + packages/hooks/src/index.ts | 1 + packages/hooks/src/useRequest/Fetch.ts | 147 ++++++++ packages/hooks/src/useRequest/index.ts | 30 ++ .../useRequest/plugins/useAutoRunPlugin.ts | 52 +++ .../src/useRequest/plugins/useCachePlugin.ts | 127 +++++++ .../useRequest/plugins/useDebouncePlugin.ts | 71 ++++ .../plugins/useLoadingDelayPlugin.ts | 45 +++ .../useRequest/plugins/usePollingPlugin.ts | 71 ++++ .../plugins/useRefreshOnWindowFocusPlugin.ts | 37 ++ .../src/useRequest/plugins/useRetryPlugin.ts | 54 +++ .../useRequest/plugins/useThrottlePlugin.ts | 63 ++++ packages/hooks/src/useRequest/types.ts | 124 +++++++ .../src/useRequest/useRequestImplement.ts | 49 +++ packages/hooks/src/useRequest/utils/cache.ts | 48 +++ .../src/useRequest/utils/cachePromise.ts | 23 ++ .../src/useRequest/utils/cacheSubscribe.ts | 22 ++ .../hooks/src/useRequest/utils/isBrowser.ts | 5 + .../src/useRequest/utils/isDocumentVisible.ts | 8 + .../hooks/src/useRequest/utils/isFunction.ts | 2 + .../hooks/src/useRequest/utils/isOnline.ts | 8 + packages/hooks/src/useRequest/utils/limit.ts | 12 + .../src/useRequest/utils/subscribeFocus.ts | 30 ++ .../useRequest/utils/subscribeReVisible.ts | 25 ++ pnpm-lock.yaml | 103 ++---- src/router/routes/modules/hooks/request.ts | 79 +++++ src/views/hooks/request/base.tsx | 328 ++++++++++++++++++ src/views/hooks/request/cache.tsx | 318 +++++++++++++++++ src/views/hooks/request/debounce.tsx | 62 ++++ src/views/hooks/request/loading-delay.tsx | 61 ++++ src/views/hooks/request/mock-api.ts | 27 ++ src/views/hooks/request/polling.tsx | 96 +++++ src/views/hooks/request/ready.tsx | 86 +++++ .../hooks/request/refresh-on-window-focus.tsx | 50 +++ src/views/hooks/request/refresy-deps.tsx | 43 +++ src/views/hooks/request/retry.tsx | 53 +++ src/views/hooks/request/throttle.tsx | 61 ++++ 37 files changed, 2357 insertions(+), 65 deletions(-) create mode 100644 packages/hooks/src/useRequest/Fetch.ts create mode 100644 packages/hooks/src/useRequest/index.ts create mode 100644 packages/hooks/src/useRequest/plugins/useAutoRunPlugin.ts create mode 100644 packages/hooks/src/useRequest/plugins/useCachePlugin.ts create mode 100644 packages/hooks/src/useRequest/plugins/useDebouncePlugin.ts create mode 100644 packages/hooks/src/useRequest/plugins/useLoadingDelayPlugin.ts create mode 100644 packages/hooks/src/useRequest/plugins/usePollingPlugin.ts create mode 100644 packages/hooks/src/useRequest/plugins/useRefreshOnWindowFocusPlugin.ts create mode 100644 packages/hooks/src/useRequest/plugins/useRetryPlugin.ts create mode 100644 packages/hooks/src/useRequest/plugins/useThrottlePlugin.ts create mode 100644 packages/hooks/src/useRequest/types.ts create mode 100644 packages/hooks/src/useRequest/useRequestImplement.ts create mode 100644 packages/hooks/src/useRequest/utils/cache.ts create mode 100644 packages/hooks/src/useRequest/utils/cachePromise.ts create mode 100644 packages/hooks/src/useRequest/utils/cacheSubscribe.ts create mode 100644 packages/hooks/src/useRequest/utils/isBrowser.ts create mode 100644 packages/hooks/src/useRequest/utils/isDocumentVisible.ts create mode 100644 packages/hooks/src/useRequest/utils/isFunction.ts create mode 100644 packages/hooks/src/useRequest/utils/isOnline.ts create mode 100644 packages/hooks/src/useRequest/utils/limit.ts create mode 100644 packages/hooks/src/useRequest/utils/subscribeFocus.ts create mode 100644 packages/hooks/src/useRequest/utils/subscribeReVisible.ts create mode 100644 src/router/routes/modules/hooks/request.ts create mode 100644 src/views/hooks/request/base.tsx create mode 100644 src/views/hooks/request/cache.tsx create mode 100644 src/views/hooks/request/debounce.tsx create mode 100644 src/views/hooks/request/loading-delay.tsx create mode 100644 src/views/hooks/request/mock-api.ts create mode 100644 src/views/hooks/request/polling.tsx create mode 100644 src/views/hooks/request/ready.tsx create mode 100644 src/views/hooks/request/refresh-on-window-focus.tsx create mode 100644 src/views/hooks/request/refresy-deps.tsx create mode 100644 src/views/hooks/request/retry.tsx create mode 100644 src/views/hooks/request/throttle.tsx diff --git a/packages/hooks/package.json b/packages/hooks/package.json index 21927a0aa..f532e2b3c 100644 --- a/packages/hooks/package.json +++ b/packages/hooks/package.json @@ -30,6 +30,7 @@ }, "dependencies": { "@vueuse/core": "^10.2.1", + "lodash-es": "^4.17.21", "vue": "^3.3.4" }, "devDependencies": { diff --git a/packages/hooks/src/index.ts b/packages/hooks/src/index.ts index 71fb0c545..557a974d8 100644 --- a/packages/hooks/src/index.ts +++ b/packages/hooks/src/index.ts @@ -1,6 +1,7 @@ export * from './onMountedOrActivated'; export * from './useAttrs'; export * from './useRefs'; +export * from './useRequest'; export * from './useScrollTo'; export * from './useWindowSizeFn'; export { useTimeoutFn } from '@vueuse/core'; diff --git a/packages/hooks/src/useRequest/Fetch.ts b/packages/hooks/src/useRequest/Fetch.ts new file mode 100644 index 000000000..5e5b40adc --- /dev/null +++ b/packages/hooks/src/useRequest/Fetch.ts @@ -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 { + pluginImpls: PluginReturn[] = []; + + count: number = 0; + + state: FetchState = reactive({ + loading: false, + params: undefined, + data: undefined, + error: undefined, + }); + + constructor( + public serviceRef: Service, + public options: UseRequestOptions, + public subscribe: Subscribe, + public initState: Partial> = {}, + ) { + this.setState({ loading: !options.manual, ...initState }); + } + + setState(s: Partial> = {}) { + Object.assign(this.state, s); + this.subscribe(); + } + + runPluginHandler(event: keyof PluginReturn, ...rest: any[]) { + // @ts-ignore + const r = this.pluginImpls.map((i) => i[event]?.(...rest)).filter(Boolean); + return Object.assign({}, ...r); + } + + async runAsync(...params: TParams): Promise { + 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 }); + } +} diff --git a/packages/hooks/src/useRequest/index.ts b/packages/hooks/src/useRequest/index.ts new file mode 100644 index 000000000..8b8c887d3 --- /dev/null +++ b/packages/hooks/src/useRequest/index.ts @@ -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( + service: Service, + options?: UseRequestOptions, + plugins?: UseRequestPlugin[], +) { + return useRequestImplement(service, options, [ + ...(plugins || []), + useDebouncePlugin, + useLoadingDelayPlugin, + usePollingPlugin, + useRefreshOnWindowFocusPlugin, + useThrottlePlugin, + useAutoRunPlugin, + useCachePlugin, + useRetryPlugin, + ] as UseRequestPlugin[]); +} diff --git a/packages/hooks/src/useRequest/plugins/useAutoRunPlugin.ts b/packages/hooks/src/useRequest/plugins/useAutoRunPlugin.ts new file mode 100644 index 000000000..0023b9be9 --- /dev/null +++ b/packages/hooks/src/useRequest/plugins/useAutoRunPlugin.ts @@ -0,0 +1,52 @@ +import { ref, unref, watch } from 'vue'; + +import type { UseRequestPlugin } from '../types'; + +// support refreshDeps & ready +const useAutoRunPlugin: UseRequestPlugin = ( + 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; diff --git a/packages/hooks/src/useRequest/plugins/useCachePlugin.ts b/packages/hooks/src/useRequest/plugins/useCachePlugin.ts new file mode 100644 index 000000000..b449b6640 --- /dev/null +++ b/packages/hooks/src/useRequest/plugins/useCachePlugin.ts @@ -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 = ( + fetchInstance, + { + cacheKey, + cacheTime = 5 * 60 * 1000, + staleTime = 0, + setCache: customSetCache, + getCache: customGetCache, + }, +) => { + const unSubscribeRef = ref<() => void>(); + const currentPromiseRef = ref>(); + + 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; diff --git a/packages/hooks/src/useRequest/plugins/useDebouncePlugin.ts b/packages/hooks/src/useRequest/plugins/useDebouncePlugin.ts new file mode 100644 index 000000000..6a91ad1e7 --- /dev/null +++ b/packages/hooks/src/useRequest/plugins/useDebouncePlugin.ts @@ -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 = ( + fetchInstance, + { debounceWait, debounceLeading, debounceTrailing, debounceMaxWait }, +) => { + const debouncedRef = ref>(); + + 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; diff --git a/packages/hooks/src/useRequest/plugins/useLoadingDelayPlugin.ts b/packages/hooks/src/useRequest/plugins/useLoadingDelayPlugin.ts new file mode 100644 index 000000000..89c77f8a0 --- /dev/null +++ b/packages/hooks/src/useRequest/plugins/useLoadingDelayPlugin.ts @@ -0,0 +1,45 @@ +import { ref, unref } from 'vue'; + +import type { UseRequestPlugin, UseRequestTimeout } from '../types'; + +const useLoadingDelayPlugin: UseRequestPlugin = ( + fetchInstance, + { loadingDelay, ready }, +) => { + const timerRef = ref(); + + 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; diff --git a/packages/hooks/src/useRequest/plugins/usePollingPlugin.ts b/packages/hooks/src/useRequest/plugins/usePollingPlugin.ts new file mode 100644 index 000000000..7d076b370 --- /dev/null +++ b/packages/hooks/src/useRequest/plugins/usePollingPlugin.ts @@ -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 = ( + fetchInstance, + { pollingInterval, pollingWhenHidden = true, pollingErrorRetryCount = -1 }, +) => { + const timerRef = ref(); + const unsubscribeRef = ref<() => void>(); + const countRef = ref(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; diff --git a/packages/hooks/src/useRequest/plugins/useRefreshOnWindowFocusPlugin.ts b/packages/hooks/src/useRequest/plugins/useRefreshOnWindowFocusPlugin.ts new file mode 100644 index 000000000..e1f7b3bbb --- /dev/null +++ b/packages/hooks/src/useRequest/plugins/useRefreshOnWindowFocusPlugin.ts @@ -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 = ( + 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; diff --git a/packages/hooks/src/useRequest/plugins/useRetryPlugin.ts b/packages/hooks/src/useRequest/plugins/useRetryPlugin.ts new file mode 100644 index 000000000..b400db35e --- /dev/null +++ b/packages/hooks/src/useRequest/plugins/useRetryPlugin.ts @@ -0,0 +1,54 @@ +import { ref } from 'vue'; + +import type { UseRequestPlugin, UseRequestTimeout } from '../types'; + +const useRetryPlugin: UseRequestPlugin = ( + fetchInstance, + { retryInterval, retryCount }, +) => { + const timerRef = ref(); + 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; diff --git a/packages/hooks/src/useRequest/plugins/useThrottlePlugin.ts b/packages/hooks/src/useRequest/plugins/useThrottlePlugin.ts new file mode 100644 index 000000000..adeed0518 --- /dev/null +++ b/packages/hooks/src/useRequest/plugins/useThrottlePlugin.ts @@ -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 = ( + fetchInstance, + { throttleWait, throttleLeading, throttleTrailing }, +) => { + const throttledRef = ref>(); + + 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; diff --git a/packages/hooks/src/useRequest/types.ts b/packages/hooks/src/useRequest/types.ts new file mode 100644 index 000000000..2a24c1ccc --- /dev/null +++ b/packages/hooks/src/useRequest/types.ts @@ -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 = (...args: TParams) => Promise; +export type Subscribe = () => void; + +// for Fetch +export interface FetchState { + loading: boolean; + params?: TParams; + data?: TData; + error?: Error; +} + +export interface PluginReturn { + onBefore?: (params: TParams) => + | ({ + stopNow?: boolean; + returnNow?: boolean; + } & Partial>) + | void; + + onRequest?: ( + service: Service, + params: TParams, + ) => { + servicePromise?: Promise; + }; + + 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 { + manual?: MaybeRef; + + 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[]; + 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) => void; + getCache?: (params: TParams) => CachedData | undefined; + + // retry + retryCount?: number; + retryInterval?: number; + + // ready + ready?: MaybeRef; + + // [key: string]: any; +} + +export interface UseRequestPlugin { + // eslint-disable-next-line prettier/prettier + (fetchInstance: Fetch, options: UseRequestOptions): PluginReturn< + TData, + TParams + >; + onInit?: (options: UseRequestOptions) => Partial>; +} + +// for index +// export type OptionsWithoutFormat = Omit, 'formatResult'>; + +// export interface OptionsWithFormat extends Omit, 'formatResult'> { +// formatResult: (res: TData) => TFormated; +// }; + +export interface UseRequestResult { + loading: Ref; + data: Ref; + error: Ref; + params: Ref; + cancel: Fetch['cancel']; + refresh: Fetch['refresh']; + refreshAsync: Fetch['refreshAsync']; + run: Fetch['run']; + runAsync: Fetch['runAsync']; + mutate: Fetch['mutate']; +} + +export type UseRequestTimeout = ReturnType; diff --git a/packages/hooks/src/useRequest/useRequestImplement.ts b/packages/hooks/src/useRequest/useRequestImplement.ts new file mode 100644 index 000000000..54cf1530d --- /dev/null +++ b/packages/hooks/src/useRequest/useRequestImplement.ts @@ -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( + service: Service, + options: UseRequestOptions = {}, + plugins: UseRequestPlugin[] = [], +) { + const { manual = false, ...rest } = options; + const fetchOptions = { manual, ...rest }; + + const initState = plugins.map((p) => p?.onInit?.(fetchOptions)).filter(Boolean); + + const fetchInstance = new Fetch( + 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; +} diff --git a/packages/hooks/src/useRequest/utils/cache.ts b/packages/hooks/src/useRequest/utils/cache.ts new file mode 100644 index 000000000..f89e0a137 --- /dev/null +++ b/packages/hooks/src/useRequest/utils/cache.ts @@ -0,0 +1,48 @@ +type Timer = ReturnType; +type CachedKey = string | number; + +export interface CachedData { + data: TData; + params: TParams; + time: number; +} + +interface RecordData extends CachedData { + timer: Timer | undefined; +} + +const cache = new Map(); + +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(); + } +}; diff --git a/packages/hooks/src/useRequest/utils/cachePromise.ts b/packages/hooks/src/useRequest/utils/cachePromise.ts new file mode 100644 index 000000000..602a5c21f --- /dev/null +++ b/packages/hooks/src/useRequest/utils/cachePromise.ts @@ -0,0 +1,23 @@ +type CachedKey = string | number; + +const cachePromise = new Map>(); + +export const getCachePromise = (cacheKey: CachedKey) => { + return cachePromise.get(cacheKey); +}; + +export const setCachePromise = (cacheKey: CachedKey, promise: Promise) => { + // 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); + }); +}; diff --git a/packages/hooks/src/useRequest/utils/cacheSubscribe.ts b/packages/hooks/src/useRequest/utils/cacheSubscribe.ts new file mode 100644 index 000000000..c66dc0c91 --- /dev/null +++ b/packages/hooks/src/useRequest/utils/cacheSubscribe.ts @@ -0,0 +1,22 @@ +type Listener = (data: any) => void; + +const listeners: Record = {}; + +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); + }; +}; diff --git a/packages/hooks/src/useRequest/utils/isBrowser.ts b/packages/hooks/src/useRequest/utils/isBrowser.ts new file mode 100644 index 000000000..4a1b91e97 --- /dev/null +++ b/packages/hooks/src/useRequest/utils/isBrowser.ts @@ -0,0 +1,5 @@ +export const isBrowser = !!( + typeof window !== 'undefined' && + window.document && + window.document.createElement +); diff --git a/packages/hooks/src/useRequest/utils/isDocumentVisible.ts b/packages/hooks/src/useRequest/utils/isDocumentVisible.ts new file mode 100644 index 000000000..e9d1275d4 --- /dev/null +++ b/packages/hooks/src/useRequest/utils/isDocumentVisible.ts @@ -0,0 +1,8 @@ +import { isBrowser } from './isBrowser'; + +export function isDocumentVisible(): boolean { + if (isBrowser) { + return document.visibilityState !== 'hidden'; + } + return true; +} diff --git a/packages/hooks/src/useRequest/utils/isFunction.ts b/packages/hooks/src/useRequest/utils/isFunction.ts new file mode 100644 index 000000000..782f37c82 --- /dev/null +++ b/packages/hooks/src/useRequest/utils/isFunction.ts @@ -0,0 +1,2 @@ +export const isFunction = (value: unknown): value is (...args: any) => any => + typeof value === 'function'; diff --git a/packages/hooks/src/useRequest/utils/isOnline.ts b/packages/hooks/src/useRequest/utils/isOnline.ts new file mode 100644 index 000000000..900f9a34c --- /dev/null +++ b/packages/hooks/src/useRequest/utils/isOnline.ts @@ -0,0 +1,8 @@ +import { isBrowser } from './isBrowser'; + +export function isOnline(): boolean { + if (isBrowser && typeof navigator.onLine !== 'undefined') { + return navigator.onLine; + } + return true; +} diff --git a/packages/hooks/src/useRequest/utils/limit.ts b/packages/hooks/src/useRequest/utils/limit.ts new file mode 100644 index 000000000..c540e87d0 --- /dev/null +++ b/packages/hooks/src/useRequest/utils/limit.ts @@ -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); + }; +} diff --git a/packages/hooks/src/useRequest/utils/subscribeFocus.ts b/packages/hooks/src/useRequest/utils/subscribeFocus.ts new file mode 100644 index 000000000..751650fd4 --- /dev/null +++ b/packages/hooks/src/useRequest/utils/subscribeFocus.ts @@ -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); + } + }; +} diff --git a/packages/hooks/src/useRequest/utils/subscribeReVisible.ts b/packages/hooks/src/useRequest/utils/subscribeReVisible.ts new file mode 100644 index 000000000..cf961e9ee --- /dev/null +++ b/packages/hooks/src/useRequest/utils/subscribeReVisible.ts @@ -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); + }; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 95ae48ec2..5a05078d4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -461,6 +461,9 @@ importers: '@vueuse/core': specifier: ^10.2.1 version: 10.2.1(vue@3.3.4) + lodash-es: + specifier: ^4.17.21 + version: 4.17.21 vue: specifier: ^3.3.4 version: 3.3.4 @@ -537,6 +540,7 @@ packages: /@babel/code-frame@7.23.4: resolution: {integrity: sha512-r1IONyb6Ia+jYR2vvIDhdWdlTGhqbBoFqLTQidzZ4kepUFH15ejXvFHxCVbtl7BOXIudsIubf4E81xeA3h3IXA==} engines: {node: '>=6.9.0'} + requiresBuild: true dependencies: '@babel/highlight': 7.23.4 chalk: 2.4.2 @@ -563,7 +567,7 @@ packages: '@babel/types': 7.22.5 '@nicolo-ribaudo/semver-v6': 6.3.3 convert-source-map: 1.9.0 - debug: registry.npmmirror.com/debug@4.3.4 + debug: 4.3.4 gensync: 1.0.0-beta.2 json5: 2.2.3 transitivePeerDependencies: @@ -714,6 +718,7 @@ packages: /@babel/helper-validator-identifier@7.22.20: resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} engines: {node: '>=6.9.0'} + requiresBuild: true dev: true optional: true @@ -936,7 +941,7 @@ packages: '@babel/helper-split-export-declaration': 7.22.6 '@babel/parser': 7.22.6 '@babel/types': 7.22.5 - debug: registry.npmmirror.com/debug@4.3.4 + debug: 4.3.4 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -1572,7 +1577,7 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 - debug: registry.npmmirror.com/debug@4.3.4 + debug: 4.3.4 espree: 9.6.1 globals: 13.20.0 ignore: 5.2.4 @@ -1617,7 +1622,7 @@ packages: engines: {node: '>=10.10.0'} dependencies: '@humanwhocodes/object-schema': 1.2.1 - debug: registry.npmmirror.com/debug@4.3.4 + debug: 4.3.4 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -2141,7 +2146,7 @@ packages: dependencies: '@iconify/iconify': 2.1.2 axios: 0.26.1(debug@4.3.4) - debug: registry.npmmirror.com/debug@4.3.4 + debug: 4.3.4 fast-glob: 3.3.0 fs-extra: 10.1.0 transitivePeerDependencies: @@ -2750,7 +2755,7 @@ packages: dependencies: '@typescript-eslint/typescript-estree': 6.6.0(typescript@5.2.2) '@typescript-eslint/utils': 6.6.0(eslint@8.48.0)(typescript@5.2.2) - debug: registry.npmmirror.com/debug@4.3.4 + debug: 4.3.4 eslint: 8.48.0 ts-api-utils: 1.0.3(typescript@5.2.2) typescript: 5.2.2 @@ -2774,7 +2779,7 @@ packages: dependencies: '@typescript-eslint/types': 6.6.0 '@typescript-eslint/visitor-keys': 6.6.0 - debug: registry.npmmirror.com/debug@4.3.4 + debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 semver: 7.5.4 @@ -3266,7 +3271,7 @@ packages: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} dependencies: - debug: registry.npmmirror.com/debug@4.3.4 + debug: 4.3.4 transitivePeerDependencies: - supports-color @@ -4553,6 +4558,17 @@ packages: resolution: {integrity: sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==} dev: true + /debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: registry.npmmirror.com/ms@2.0.0 + dev: true + /debug@3.2.7(supports-color@5.5.0): resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: @@ -5237,7 +5253,7 @@ packages: /eslint-import-resolver-node@0.3.7: resolution: {integrity: sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==} dependencies: - debug: registry.npmmirror.com/debug@3.2.7 + debug: 3.2.7(supports-color@5.5.0) is-core-module: 2.13.0 resolve: 1.22.2 transitivePeerDependencies: @@ -5266,7 +5282,7 @@ packages: optional: true dependencies: '@typescript-eslint/parser': 6.6.0(eslint@8.48.0)(typescript@5.2.2) - debug: registry.npmmirror.com/debug@3.2.7 + debug: 3.2.7(supports-color@5.5.0) eslint: 8.48.0 eslint-import-resolver-node: 0.3.7 transitivePeerDependencies: @@ -5554,7 +5570,7 @@ packages: resolution: {integrity: sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==} engines: {node: '>=0.10.0'} dependencies: - debug: registry.npmmirror.com/debug@2.6.9 + debug: 2.6.9 define-property: 0.2.5 extend-shallow: 2.0.1 posix-character-classes: 0.1.1 @@ -5711,7 +5727,7 @@ packages: resolution: {integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==} engines: {node: '>= 0.8'} dependencies: - debug: registry.npmmirror.com/debug@2.6.9 + debug: 2.6.9 encodeurl: 1.0.2 escape-html: 1.0.3 on-finished: 2.3.0 @@ -5950,7 +5966,7 @@ packages: dependencies: '@tootallnate/once': 1.1.2 data-uri-to-buffer: 3.0.1 - debug: registry.npmmirror.com/debug@4.3.4 + debug: 4.3.4 file-uri-to-path: 2.0.0 fs-extra: 8.1.0 ftp: 0.3.10 @@ -6344,7 +6360,7 @@ packages: dependencies: '@tootallnate/once': 1.1.2 agent-base: 6.0.2 - debug: registry.npmmirror.com/debug@4.3.4 + debug: 4.3.4 transitivePeerDependencies: - supports-color @@ -6353,7 +6369,7 @@ packages: engines: {node: '>= 6'} dependencies: agent-base: 6.0.2 - debug: registry.npmmirror.com/debug@4.3.4 + debug: 4.3.4 transitivePeerDependencies: - supports-color @@ -6868,7 +6884,7 @@ packages: resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} engines: {node: '>=10'} dependencies: - debug: registry.npmmirror.com/debug@4.3.4 + debug: 4.3.4 istanbul-lib-coverage: 3.2.0 source-map: registry.npmmirror.com/source-map@0.6.1 transitivePeerDependencies: @@ -8720,7 +8736,7 @@ packages: dependencies: '@tootallnate/once': 1.1.2 agent-base: 6.0.2 - debug: registry.npmmirror.com/debug@4.3.4 + debug: 4.3.4 get-uri: 3.0.2 http-proxy-agent: 4.0.1 https-proxy-agent: 5.0.1 @@ -9262,7 +9278,7 @@ packages: engines: {node: '>= 8'} dependencies: agent-base: 6.0.2 - debug: registry.npmmirror.com/debug@4.3.4 + debug: 4.3.4 http-proxy-agent: 4.0.1 https-proxy-agent: 5.0.1 lru-cache: 5.1.1 @@ -9509,7 +9525,7 @@ packages: resolution: {integrity: sha512-efCx3b+0Z69/LGJmm9Yvi4cqEdxnoGnxYxGxBghkkTTFeXRtTCmmhO0AnAfHz59k957uTSuy8WaHqOs8wbYUWg==} engines: {node: '>=6'} dependencies: - debug: registry.npmmirror.com/debug@4.3.4 + debug: 4.3.4 module-details-from-path: 1.0.3 resolve: 1.22.2 transitivePeerDependencies: @@ -9970,7 +9986,7 @@ packages: engines: {node: '>=0.10.0'} dependencies: base: 0.11.2 - debug: registry.npmmirror.com/debug@2.6.9 + debug: 2.6.9 define-property: 0.2.5 extend-shallow: 2.0.1 map-cache: 0.2.2 @@ -9986,7 +10002,7 @@ packages: engines: {node: '>= 6'} dependencies: agent-base: 6.0.2 - debug: registry.npmmirror.com/debug@4.3.4 + debug: 4.3.4 socks: 2.7.1 transitivePeerDependencies: - supports-color @@ -12247,7 +12263,7 @@ packages: dom-align: registry.npmmirror.com/dom-align@1.12.4 dom-scroll-into-view: registry.npmmirror.com/dom-scroll-into-view@2.0.1 lodash: registry.npmmirror.com/lodash@4.17.21 - lodash-es: registry.npmmirror.com/lodash-es@4.17.21 + lodash-es: 4.17.21 resize-observer-polyfill: registry.npmmirror.com/resize-observer-polyfill@1.5.1 scroll-into-view-if-needed: registry.npmmirror.com/scroll-into-view-if-needed@2.2.31 shallow-equal: registry.npmmirror.com/shallow-equal@1.2.1 @@ -12325,32 +12341,6 @@ packages: ms: registry.npmmirror.com/ms@2.0.0 dev: true - registry.npmmirror.com/debug@3.2.7: - resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz} - name: debug - version: 3.2.7 - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: registry.npmmirror.com/ms@2.1.3 - dev: true - - registry.npmmirror.com/debug@4.3.4: - resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/debug/-/debug-4.3.4.tgz} - name: debug - version: 4.3.4 - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: registry.npmmirror.com/ms@2.1.2 - registry.npmmirror.com/dom-align@1.12.4: resolution: {integrity: sha512-R8LUSEay/68zE5c8/3BDxiTEvgb4xZTF0RKmAHfiEVN3klfIpXfi2/QCoiWPccVQ0J/ZGdz9OjzL4uJEP/MRAw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/dom-align/-/dom-align-1.12.4.tgz} name: dom-align @@ -12410,12 +12400,6 @@ packages: name: js-tokens version: 4.0.0 - registry.npmmirror.com/lodash-es@4.17.21: - resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz} - name: lodash-es - version: 4.17.21 - dev: false - registry.npmmirror.com/lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz} name: lodash @@ -12445,17 +12429,6 @@ packages: version: 2.0.0 dev: true - registry.npmmirror.com/ms@2.1.2: - resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/ms/-/ms-2.1.2.tgz} - name: ms - version: 2.1.2 - - registry.npmmirror.com/ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz} - name: ms - version: 2.1.3 - dev: true - registry.npmmirror.com/nanopop@2.3.0: resolution: {integrity: sha512-fzN+T2K7/Ah25XU02MJkPZ5q4Tj5FpjmIYq4rvoHX4yb16HzFdCO6JxFFn5Y/oBhQ8no8fUZavnyIv9/+xkBBw==, registry: https://registry.npm.taobao.org/, tarball: https://registry.npmmirror.com/nanopop/-/nanopop-2.3.0.tgz} name: nanopop diff --git a/src/router/routes/modules/hooks/request.ts b/src/router/routes/modules/hooks/request.ts new file mode 100644 index 000000000..fd75fc717 --- /dev/null +++ b/src/router/routes/modules/hooks/request.ts @@ -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; diff --git a/src/views/hooks/request/base.tsx b/src/views/hooks/request/base.tsx new file mode 100644 index 000000000..d685db303 --- /dev/null +++ b/src/views/hooks/request/base.tsx @@ -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 () => ( + + + + useRequest + 的第一个参数是一个异步函数,在组件初次加载时,会自动触发该函数执行。同时自动管理该异步函数的 + loading + data + error + 等状态。 + + + {`const { data, error, loading } = useRequest(imitateApi);`} + + + + {/* 基础案例 */} + +
{error.value ? 'failed to load' : `Username: ${data.value}`}
+
+
+ ); + }, +}); + +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 () => ( + + + + 如果设置了 + options.manual = true + ,则 useRequest 不会默认执行,需要通过 + run 来触发执行。 + + + {`const { loading, run } = useRequest(imitateApi, { manual: true });`} + + + + {/* 手动触发 */} + + + + + + ); + }, +}); + +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 () => ( + + + + useRequest + 提供了以下几个生命周期配置项,供你在异步函数的不同阶段做一些处理。 + + + + onBefore + 请求之前触发 + + + + onSuccess + 请求成功触发 + + + + onError + 请求失败触发 + + + + onFinally + 请求完成触发 + + + + {/* 生命周期 */} + + + + + + + ); + }, +}); + +const Demo4 = defineComponent({ + setup() { + const { data, loading, run, refresh } = useRequest(imitateApi, { + manual: true, + }); + + onMounted(() => run('lutz')); + + const changeData = () => { + data.value = `${Date.now()}`; + }; + + return () => ( + + + + useRequest + 提供了 + refresh 和 + refreshAsync + 方法,使我们可以使用上一次的参数,重新发起请求。 + + + + + +
Username: {data.value}
+ + +
+
+
+ ); + }, +}); + +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 () => ( + + + + useRequest 提供了 + cancel 函数,用于忽略当前 promise + 返回的数据和错误 + + + + {/* 取消响应 */} + + + + + + + ); + }, +}); + +const Demo6 = defineComponent({ + setup() { + const search = ref(''); + + const { + data: username, + loading, + run, + params, + } = useRequest(imitateApi, { + defaultParams: ['lutz'], + }); + + const onChange = () => { + run(search.value); + }; + + return () => ( + + + + useRequest 返回的 + params 会记录当次调用 + service 的参数数组。比如你触发了 + run(1, 2, 3),则 + params 等于 + [1, 2, 3] + + + 如果我们设置了 + options.manual = false ,则首次调用 + service + 的参数可以通过 options.defaultParams + 来设置。 + + + + {/* 管理参数 */} + + + + +
+
UserId: {unref(params)?.[0]}
+
Username: {unref(username)}
+
+
+ ); + }, +}); + +export default defineComponent({ + setup() { + return () => ( + ( + + + ahooks{' '} + + useRequest 的 vue 版本,是一个强大的异步数据管理的 Hooks。 + +
    + {[ + '自动请求/手动请求', + '轮询', + '防抖', + '节流', + '屏幕聚焦重新请求', + '错误重试', + 'loading delay', + 'SWR(stale-while-revalidate)', + '缓存', + ].map((item) => ( +
  • + {item} +
  • + ))} +
+
+
+ ), + }} + > + + + + + + +
+ ); + }, +}); diff --git a/src/views/hooks/request/cache.tsx b/src/views/hooks/request/cache.tsx new file mode 100644 index 000000000..cc9c4a17c --- /dev/null +++ b/src/views/hooks/request/cache.tsx @@ -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 () => ( + <> +

Background loading: {loading.value ? 'true' : 'false'}

+

Latest request time: {unref(data)?.time}

+

{unref(data)?.data}

+ + ); + }, +}); + +const Demo1 = defineComponent({ + setup() { + const state = ref(false); + const toggle = (bool?: boolean) => { + state.value = bool ?? !state.value; + }; + + return () => ( + + + + 下面的示例,我们设置了 + cacheKey + ,在组件第二次加载时,会优先返回缓存的内容,然后在背后重新发起请求。你可以通过点击按钮来体验效果。 + + + + {/* SWR */} +
+ + {state.value && } +
+
+ ); + }, +}); + +const Article2 = defineComponent({ + setup() { + const { loading, data } = useRequest(getArticle, { + cacheKey: 'staleTime-demo', + staleTime: 5000, + }); + + return () => ( + <> +

Background loading: {loading.value ? 'true' : 'false'}

+

Latest request time: {unref(data)?.time}

+

{unref(data)?.data}

+ + ); + }, +}); + +const Demo2 = defineComponent({ + setup() { + const state = ref(false); + const toggle = (bool?: boolean) => { + state.value = bool ?? !state.value; + }; + + return () => ( + + + + 通过设置 + staleTime + ,我们可以指定数据新鲜时间,在这个时间内,不会重新发起请求。下面的示例设置了 5s + 的新鲜时间,你可以通过点击按钮来体验效果 + + + + {/* 数据保持新鲜 */} +
+ + {state.value && } +
+
+ ); + }, +}); + +const Article3 = defineComponent({ + setup() { + const { loading, data, refresh } = useRequest(getArticle, { + cacheKey: 'cacheKey-share', + }); + + return () => ( + <> +

Background loading: {loading.value ? 'true' : 'false'}

+ +

Latest request time: {unref(data)?.time}

+

{unref(data)?.data}

+ + ); + }, +}); + +const Demo3 = defineComponent({ + setup() { + return () => ( + + + + 同一个 cacheKey + 的内容,在全局是共享的,这会带来以下几个特性 + + + +
    +
  • + 请求 Promise 共享,相同的 cacheKey + 同时只会有一个在发起请求,后发起的会共用同一个请求 Promise +
  • +
  • + 数据同步,任何时候,当我们改变其中某个 cacheKey 的内容时,其它相同 + cacheKey + 的内容均会同步 +
  • +
+
+
+ + {/* 数据共享 */} +
+

Article 1

+ +

Article 2

+ +
+
+ ); + }, +}); + +const Article4 = defineComponent({ + setup() { + const { loading, data, params, run } = useRequest(getArticle, { + cacheKey: 'cacheKey-share4', + }); + + const keyword = ref(params.value?.[0] || ''); + + return () => ( + <> + + + + +

Background loading: {loading.value ? 'true' : 'false'}

+

Latest request time: {unref(data)?.time}

+

Latest request data: {unref(data)?.data}

+

keyword: {keyword.value}

+ + ); + }, +}); + +const Demo4 = defineComponent({ + setup() { + const state = ref(false); + const toggle = (bool?: boolean) => { + state.value = bool ?? !state.value; + }; + + return () => ( + + + + 缓存的数据包括 data 和 params,通过 params + 缓存机制,我们可以记忆上一次请求的条件,并在下次初始化 + + + + {/* 参数缓存 */} +
+ +
{state.value && }
+
+
+ ); + }, +}); + +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 () => ( + + + + useRequest 提供了一个 clearCache 方法,可以清除指定 cacheKey 的缓存数据。 + + + + {/* 删除缓存 */} +
+ + + + + + + +

Article 1

+ {state.value && } +

Article 2

+ {state.value && } +

Article 3

+ {state.value && } +
+
+ ); + }, +}); + +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 () => ( + <> +

Background loading: {loading.value ? 'true' : 'false'}

+

Latest request time: {unref(data)?.time}

+

{unref(data)?.data}

+ + ); + }, +}); + +const Demo6 = defineComponent({ + setup() { + const state = ref(false); + const toggle = (bool?: boolean) => { + state.value = bool ?? !state.value; + }; + + return () => ( + + + + 通过配置 setCache 和 getCache,可以自定义数据缓存,比如可以将数据存储到 + localStorage、IndexDB 等。 + + + + {/* 自定义缓存 */} +
+ +
{state.value && }
+
+
+ ); + }, +}); + +export default defineComponent({ + setup() { + return () => ( + + + + + + + + + ); + }, +}); diff --git a/src/views/hooks/request/debounce.tsx b/src/views/hooks/request/debounce.tsx new file mode 100644 index 000000000..71b3e8db0 --- /dev/null +++ b/src/views/hooks/request/debounce.tsx @@ -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 () => ( + + + + 通过设置 options.debounceWait + ,进入防抖模式,此时如果频繁触发 + run + 或者 + runAsync + 则会以防抖策略进行请求。 + + + + + {`const { data, run } = useRequest(imitateApi, { debounceWait: 300, manual: true });`} + + + + + 如上示例代码,频繁触发 + run , 300ms 执行一次。 + + + 你可以在下面 input 框中快速输入文本,体验效果 + + + {/* 防抖 */} + + + +
Username: {data.value}
+
+
+
+ ); + }, +}); + +export default defineComponent({ + setup() { + return () => ( + + + + ); + }, +}); diff --git a/src/views/hooks/request/loading-delay.tsx b/src/views/hooks/request/loading-delay.tsx new file mode 100644 index 000000000..36f0aed3c --- /dev/null +++ b/src/views/hooks/request/loading-delay.tsx @@ -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 () => ( + + + + + 通过设置 + options.loadingDelay + 可以延迟 loading 变成 + true + 的时间,有效防止闪烁。 + + + + + {`const { loading, data } = useRequest(imitateApi, { loadingDelay: 300 });`} + + + + + 例如上面的场景,假如 imitateApi 在 300ms 内返回,则{' '} + loading 不会变成{' '} + true Loading... 的情况。 + + + + + + +
Username: {unref(action.loading) ? 'Loading...' : unref(action.data)}
+ +
+ Username:{' '} + {unref(withLoadingDelayAction.loading) + ? 'Loading...' + : unref(withLoadingDelayAction.data)} +
+
+
+
+ ); + }, +}); diff --git a/src/views/hooks/request/mock-api.ts b/src/views/hooks/request/mock-api.ts new file mode 100644 index 000000000..1ef4672b1 --- /dev/null +++ b/src/views/hooks/request/mock-api.ts @@ -0,0 +1,27 @@ +import Mock from 'mockjs'; + +export async function imitateApi(username?: string, pass: boolean = true): Promise { + 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); + }); +} diff --git a/src/views/hooks/request/polling.tsx b/src/views/hooks/request/polling.tsx new file mode 100644 index 000000000..0ba033ba2 --- /dev/null +++ b/src/views/hooks/request/polling.tsx @@ -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 () => ( + + + + 通过设置 + options.pollingInterval + ,进入轮询模式,useRequest 会定时触发 service 执行。 + + + + {`const { data, run, cancel } = useRequest(imitateApi, { pollingInterval: 3000 });`} + + + + +
+
Username: {loading.value ? 'Loading' : data.value}
+ + + + +
+
+ ); + }, +}); + +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 () => ( + + + + 通过 + options.pollingErrorRetryCount + 轮询错误重试次数。 + + + + {`const { data, run, cancel } = useRequest(imitateApi, { pollingInterval: 3000, pollingErrorRetryCount: 3 });`} + + + + +
+
Username: {loading.value ? 'Loading' : data.value}
+ + + + +
+
+ ); + }, +}); + +export default defineComponent({ + setup() { + return () => ( + + + + + ); + }, +}); diff --git a/src/views/hooks/request/ready.tsx b/src/views/hooks/request/ready.tsx new file mode 100644 index 000000000..8d8f283a4 --- /dev/null +++ b/src/views/hooks/request/ready.tsx @@ -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 () => ( + + + + 以下示例演示了自动模式下 + ready 的行为。每次 + ready 从 false 变为 true + 时,都会重新发起请求。 + + + +
+ +
Ready: {JSON.stringify(unref(ready))}
+ +
+
Username: {loading.value ? 'Loading' : unref(data)}
+
+
+ ); + }, +}); + +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 () => ( + + + + 以下示例演示了手动模式下 + ready + 的行为。只有当 + ready + 等于 true 时,run 才会执行。 + + + +
+ +
Ready: {JSON.stringify(unref(ready))}
+ +
+
+ +
Username: {loading.value ? 'Loading' : unref(data)}
+ +
+
+
+
+ ); + }, +}); + +export default defineComponent({ + setup() { + return () => ( + + + + + ); + }, +}); diff --git a/src/views/hooks/request/refresh-on-window-focus.tsx b/src/views/hooks/request/refresh-on-window-focus.tsx new file mode 100644 index 000000000..4d2b352d6 --- /dev/null +++ b/src/views/hooks/request/refresh-on-window-focus.tsx @@ -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 () => ( + + + + 通过设置 options.refreshOnWindowFocus + ,在浏览器窗口 refocus 和 revisible 时, 会重新发起请求。 + + + + + {`const { data, run } = useRequest(imitateApi, { refreshOnWindowFocus: true });`} + + + + + 你可以点击浏览器外部,再点击当前页面来体验效果(或者隐藏当前页面,重新展示),如果和上一次请求间隔大于 + 5000ms, 则会重新请求一次。 + + + + {/* 屏幕聚焦重新请求 */} + +
Username: {data.value}
+
+
+ ); + }, +}); + +export default defineComponent({ + setup() { + return () => ( + + + + ); + }, +}); diff --git a/src/views/hooks/request/refresy-deps.tsx b/src/views/hooks/request/refresy-deps.tsx new file mode 100644 index 000000000..2c9dd74c0 --- /dev/null +++ b/src/views/hooks/request/refresy-deps.tsx @@ -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 () => ( + + + + useRequest 提供了一个 + options.refreshDeps + 参数,当它的值变化后,会重新触发请求。 + + + + + + + + ); + }, +}); + +export default defineComponent({ + setup() { + return () => ( + + + + ); + }, +}); diff --git a/src/views/hooks/request/throttle.tsx b/src/views/hooks/request/throttle.tsx new file mode 100644 index 000000000..6be53a6cf --- /dev/null +++ b/src/views/hooks/request/throttle.tsx @@ -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 () => ( + + + + 通过设置 + options.throttleWait + ,进入节流模式,此时如果频繁触发 + run 或者 + runAsync , 则会以节流策略进行请求。 + + + + + {`const { data, run } = useRequest(imitateApi, { throttleWait: 300, manual: true });`} + + + + + 如上示例代码,频繁触发 + run , 300ms 执行一次。 + + + 你可以在下面 input 框中快速输入文本,体验效果 + + + {/* 节流 */} + + + +
Username: {data.value}
+
+
+
+ ); + }, +}); + +export default defineComponent({ + setup() { + return () => ( + + + + ); + }, +});