Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: hooks useRequest 异步数据管理 #3447

Merged
merged 1 commit into from
Dec 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/hooks/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
},
"dependencies": {
"@vueuse/core": "^10.2.1",
"lodash-es": "^4.17.21",
"vue": "^3.3.4"
},
"devDependencies": {
Expand Down
1 change: 1 addition & 0 deletions packages/hooks/src/index.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 147 additions & 0 deletions packages/hooks/src/useRequest/Fetch.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 30 additions & 0 deletions packages/hooks/src/useRequest/index.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 52 additions & 0 deletions packages/hooks/src/useRequest/plugins/useAutoRunPlugin.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 127 additions & 0 deletions packages/hooks/src/useRequest/plugins/useCachePlugin.ts
Original file line number Diff line number Diff line change
@@ -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;
Loading