mirror of
https://github.com/vbenjs/vue-vben-admin.git
synced 2025-08-27 02:19:32 +08:00
feat(demo): hooks useRequest 异步数据管理 (#3447)
This commit is contained in:
@@ -30,6 +30,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@vueuse/core": "^10.2.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"vue": "^3.3.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@@ -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';
|
||||
|
147
packages/hooks/src/useRequest/Fetch.ts
Normal file
147
packages/hooks/src/useRequest/Fetch.ts
Normal file
@@ -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 });
|
||||
}
|
||||
}
|
30
packages/hooks/src/useRequest/index.ts
Normal file
30
packages/hooks/src/useRequest/index.ts
Normal file
@@ -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>[]);
|
||||
}
|
52
packages/hooks/src/useRequest/plugins/useAutoRunPlugin.ts
Normal file
52
packages/hooks/src/useRequest/plugins/useAutoRunPlugin.ts
Normal file
@@ -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;
|
127
packages/hooks/src/useRequest/plugins/useCachePlugin.ts
Normal file
127
packages/hooks/src/useRequest/plugins/useCachePlugin.ts
Normal file
@@ -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;
|
71
packages/hooks/src/useRequest/plugins/useDebouncePlugin.ts
Normal file
71
packages/hooks/src/useRequest/plugins/useDebouncePlugin.ts
Normal file
@@ -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;
|
71
packages/hooks/src/useRequest/plugins/usePollingPlugin.ts
Normal file
71
packages/hooks/src/useRequest/plugins/usePollingPlugin.ts
Normal file
@@ -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;
|
54
packages/hooks/src/useRequest/plugins/useRetryPlugin.ts
Normal file
54
packages/hooks/src/useRequest/plugins/useRetryPlugin.ts
Normal file
@@ -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;
|
63
packages/hooks/src/useRequest/plugins/useThrottlePlugin.ts
Normal file
63
packages/hooks/src/useRequest/plugins/useThrottlePlugin.ts
Normal file
@@ -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;
|
124
packages/hooks/src/useRequest/types.ts
Normal file
124
packages/hooks/src/useRequest/types.ts
Normal file
@@ -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>;
|
49
packages/hooks/src/useRequest/useRequestImplement.ts
Normal file
49
packages/hooks/src/useRequest/useRequestImplement.ts
Normal file
@@ -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>;
|
||||
}
|
48
packages/hooks/src/useRequest/utils/cache.ts
Normal file
48
packages/hooks/src/useRequest/utils/cache.ts
Normal file
@@ -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();
|
||||
}
|
||||
};
|
23
packages/hooks/src/useRequest/utils/cachePromise.ts
Normal file
23
packages/hooks/src/useRequest/utils/cachePromise.ts
Normal file
@@ -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);
|
||||
});
|
||||
};
|
22
packages/hooks/src/useRequest/utils/cacheSubscribe.ts
Normal file
22
packages/hooks/src/useRequest/utils/cacheSubscribe.ts
Normal file
@@ -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);
|
||||
};
|
||||
};
|
5
packages/hooks/src/useRequest/utils/isBrowser.ts
Normal file
5
packages/hooks/src/useRequest/utils/isBrowser.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export const isBrowser = !!(
|
||||
typeof window !== 'undefined' &&
|
||||
window.document &&
|
||||
window.document.createElement
|
||||
);
|
8
packages/hooks/src/useRequest/utils/isDocumentVisible.ts
Normal file
8
packages/hooks/src/useRequest/utils/isDocumentVisible.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { isBrowser } from './isBrowser';
|
||||
|
||||
export function isDocumentVisible(): boolean {
|
||||
if (isBrowser) {
|
||||
return document.visibilityState !== 'hidden';
|
||||
}
|
||||
return true;
|
||||
}
|
2
packages/hooks/src/useRequest/utils/isFunction.ts
Normal file
2
packages/hooks/src/useRequest/utils/isFunction.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const isFunction = (value: unknown): value is (...args: any) => any =>
|
||||
typeof value === 'function';
|
8
packages/hooks/src/useRequest/utils/isOnline.ts
Normal file
8
packages/hooks/src/useRequest/utils/isOnline.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { isBrowser } from './isBrowser';
|
||||
|
||||
export function isOnline(): boolean {
|
||||
if (isBrowser && typeof navigator.onLine !== 'undefined') {
|
||||
return navigator.onLine;
|
||||
}
|
||||
return true;
|
||||
}
|
12
packages/hooks/src/useRequest/utils/limit.ts
Normal file
12
packages/hooks/src/useRequest/utils/limit.ts
Normal file
@@ -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);
|
||||
};
|
||||
}
|
30
packages/hooks/src/useRequest/utils/subscribeFocus.ts
Normal file
30
packages/hooks/src/useRequest/utils/subscribeFocus.ts
Normal file
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
25
packages/hooks/src/useRequest/utils/subscribeReVisible.ts
Normal file
25
packages/hooks/src/useRequest/utils/subscribeReVisible.ts
Normal file
@@ -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);
|
||||
};
|
||||
}
|
103
pnpm-lock.yaml
generated
103
pnpm-lock.yaml
generated
@@ -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
|
||||
|
79
src/router/routes/modules/hooks/request.ts
Normal file
79
src/router/routes/modules/hooks/request.ts
Normal file
@@ -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;
|
328
src/views/hooks/request/base.tsx
Normal file
328
src/views/hooks/request/base.tsx
Normal file
@@ -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>
|
||||
);
|
||||
},
|
||||
});
|
318
src/views/hooks/request/cache.tsx
Normal file
318
src/views/hooks/request/cache.tsx
Normal file
@@ -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>
|
||||
);
|
||||
},
|
||||
});
|
62
src/views/hooks/request/debounce.tsx
Normal file
62
src/views/hooks/request/debounce.tsx
Normal file
@@ -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>
|
||||
);
|
||||
},
|
||||
});
|
61
src/views/hooks/request/loading-delay.tsx
Normal file
61
src/views/hooks/request/loading-delay.tsx
Normal file
@@ -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>
|
||||
);
|
||||
},
|
||||
});
|
27
src/views/hooks/request/mock-api.ts
Normal file
27
src/views/hooks/request/mock-api.ts
Normal file
@@ -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);
|
||||
});
|
||||
}
|
96
src/views/hooks/request/polling.tsx
Normal file
96
src/views/hooks/request/polling.tsx
Normal file
@@ -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>
|
||||
);
|
||||
},
|
||||
});
|
86
src/views/hooks/request/ready.tsx
Normal file
86
src/views/hooks/request/ready.tsx
Normal file
@@ -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>
|
||||
);
|
||||
},
|
||||
});
|
50
src/views/hooks/request/refresh-on-window-focus.tsx
Normal file
50
src/views/hooks/request/refresh-on-window-focus.tsx
Normal file
@@ -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>
|
||||
);
|
||||
},
|
||||
});
|
43
src/views/hooks/request/refresy-deps.tsx
Normal file
43
src/views/hooks/request/refresy-deps.tsx
Normal file
@@ -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>
|
||||
);
|
||||
},
|
||||
});
|
53
src/views/hooks/request/retry.tsx
Normal file
53
src/views/hooks/request/retry.tsx
Normal file
@@ -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>
|
||||
);
|
||||
},
|
||||
});
|
61
src/views/hooks/request/throttle.tsx
Normal file
61
src/views/hooks/request/throttle.tsx
Normal file
@@ -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>
|
||||
);
|
||||
},
|
||||
});
|
Reference in New Issue
Block a user