开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第2天,点击查看活动详情
前言
之前大致说了下useRequest hook的基本功能的实现。但useRequest的强大不止于此。它还支持如loading状态延时、请求防抖、节流、依赖刷新等功能。不过其实现方式都是通过内置的插件hook来进行实现的。这样做既可以保证核心代码的简洁,还能更方便的扩展出更高级的功能。并且还支持用户进行自定义插件。
正文
如何定义一个插件
可以通过插件 hook 的ts定义,看一下怎么定义一个插件:
插件hook接收两个参数,
- fetchInstance: fetch实例;
- fetchOptions: useRequest接收的options;
支持返回一些回调函数:
- onBefore: 在请求接口前调用;
- onRequest: 在请求接口时调用;
- onSuccess: 在请求接口成功后调用;
- onError: 在请求接口失败后调用;
- onFinally: 在请求接口完成后调用;
- onCancel: 在请求接口取消后调用;
- onMutate: 在出发mutate函数时调用;
插件hook可以返回一系列的生命周期函数,使我们可以在接口请求的任意一个时机对fetchInstance进行处理。
插件hook还支持挂载一个静态方法 onInit(),在Fetch创建之前,可以通过Init方法进行一些前提处理。得到初始的state。
const use***plugin = (
fetchInstance,
fetchOptions,
) => {
const onBefore = () => {
// 对fetchInstance的状态的处理...
}
...
return {
onBefore,
...
}
}
use***plugin.onInit = (fetchOptions) => {
...
}
export default use***plugin
插件的使用
插件hook一般用来处理Fetch实例的状态,在Fetch实例被创建之前,会先执行所有插件可能存在的onInit方法,该方法会返回initData,用来初始化Fetch实例。
在Fetch实例被初始化后,执行所有的插件hooks,此时,会得到一个包含(onBrfore、onSuccess…)的数组。并将这些方法挂载到fetchInstance上。方便在各个时机使用。
const useRequestImplement = (service: Promise<any>, options, plugins: Array<Plugin>) => {
// const { manual = false, ...rest } = options;
// const fetchOptions = {
// manual,
// ...rest,
// };
// const serviceRef = useLatest(service);
// const update = useUpdate();
const fetchInstance = useCreation(() => {
// 在Filter方法中的Boolean可以当成一个转换函数,用来筛选掉空的initData。
const initState = plugins.map((p) => p?.onInit?.(fetchOptions)).filter(Boolean);
// 将初始化的initState传给构造函数Fetch。
return new Fetch(
serviceRef,
fetchOptions,
update,
Object.assign({}, ...initState)
);
}, []);
// fetchInstance.options = fetchOptions;
// 执行所有插件hook,并将其挂载到fetchInstance上。
fetchInstance.pluginImpls = plugins.map((p) => p(fetchInstance, fetchOptions));
// useMount(() => {
// // useCachePlugin can set fetchInstance.state.params from cache when init
// const params = fetchInstance.state.params ?? [];
// // @ts-ignore
// fetchInstance.runAsync(...params);
// });
// useUnmount(() => {
// fetchInstance.cancel();
// });
// return {
// loading: fetchInstance.state.loading,
// data: fetchInstance.state.data,
// error: fetchInstance.state.error,
// params: fetchInstance.state.params || [],
// cancel: useMemoizedFn(fetchInstance.cancel.bind(fetchInstance)),
// };
};
export default class Fetch<TData, TParams extends any[]> {
// public count: number = 0;
//
// public state = {
// loading: false,
// params: undefined,
// data: undefined,
// error: undefined,
// };
public pluginImpls: Array<PluginReturn>
constructor(
public serviceRef,
public options,
public subscribe,
public initState
) {
this.state = {
...this.state,
loading: !options.manual,
...initState, //在constructor时会讲插件初始化生成的状态更新到state中。
};
}
// setState(s) {
// this.state = {
// ...this.state,
// ...s,
// };
// this.subscribe();
// }
// 插件处理函数,会批量执行某个生命周期的函数。并返回处理后的状态。
runPluginHandler(event: keyof PluginReturn, ...rest: any[]) {
// @ts-ignore
const r = this.pluginImpls.map((i) => i[event]?.(...rest)).filter(Boolean);
return Object.assign({}, ...r);
}
// async runAsync(...params: TParams): Promise<any> {
// this.count += 1;
// const currentCount = this.count;
// 执行所有插件hooks返回的‘onBefore’方法,并根据返回的state进行相应的处理
const {
stopNow = false,
returnNow = false,
...state
} = this.runPluginHandler('onBefore', params);
if (stopNow) {
return new Promise(() => {});
}
//
// this.setState({
// loading: true,
// params,
...state,
// });
//
// this.options.onBefore?.(params);
// try {
// const servicePromise = this.serviceRef.current(...params);
// const res = await servicePromise;
// if (currentCount !== this.count) {
// return new Promise(() => {});
// }
// this.setState({
// data: res,
// error: undefined,
// loading: false,
// });
//
// this.options.onSuccess?.(res, params);
// this.options.onFinally?.(params, res, undefined);
this.runPluginHandler('onSuccess', res, params);
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.options.onFinally?.(params, undefined, error);
this.runPluginHandler('onError', error, params);
if (currentCount === this.count) {
this.runPluginHandler('onFinally', params, undefined, error);
}
// throw error;
// }
// }
// cancel() {
// this.count += 1;
// this.setState({
// loading: false,
// });
this.runPluginHandler('onCancel');
// }
//
// refreshAsync() {
// // @ts-ignore
// return this.runAsync(...(this.state.params || []));
// }
// run(...params: TParams) {
// this.runAsync(...params).catch((error) => {
// if (!this.options.onError) {
// console.error(error);
// }
// });
// }
// refresh() {
// // @ts-ignore
// this.run(...(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,
// });
}
}
大概了解了插件hook的运行机制。其抛出一系列的回调函数,在接口请求的各个时机执行,对fetchInstance进行处理
Loading Delay
useRequest有个loading delay的功能,通过设置 options.loadingDelay ,可以延迟 loading 变成 true 的时间,有效防止闪烁。
这个内置hook插件用来控制loading状态的延迟。
const { loading, data } = useRequest(getUsername, {
loadingDelay: 300
});
return <div>{ loading ? 'Loading...' : data }</div>
例如上面的场景,假如 getUsername 在 300ms 内返回,则 loading 不会变成 true,避免了页面展示 Loading… 的情况。
我们可以尝试着通过插件hook的方式来实现一下。默认情况下,loading状态在接口onBefore的时候会被更新成 true。那我们就需要在接口onBefore的时候,将其改为false,并加一个定时器,让其在指定时间之后再变为true:
import { useRef } from 'react';
import type { Plugin, Timeout } from '../types';
const useLoadingDelayPlugin: Plugin<any, any[]> = (fetchInstance, { loadingDelay }) => {
const timerRef = useRef<Timeout>();
// 用户没有设置loadingDelay时直接返回
if (!loadingDelay) {
return {};
}
// 取消定时器。
const cancelTimeout = () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
};
return {
// 更改loading的状态为false,并加个定时器,让其在指定的delay时间后在更正为true。
onBefore: () => {
cancelTimeout();
timerRef.current = setTimeout(() => {
fetchInstance.setState({
loading: true,
});
}, loadingDelay);
return {
loading: false,
};
},
// 在请求完成或取消时,记得销毁定时器。
onFinally: () => {
cancelTimeout();
},
onCancel: () => {
cancelTimeout();
},
};
};
export default useLoadingDelayPlugin;
…
ready 和 refreshDeps
useRequest支持ready配置,当options中的ready值为 false
时,请求永远都不会发出。其具体行为如下:
- 当
manual=false
自动请求模式时,每次ready
从false
变为true
时,都会自动发起请求,会带上参数options.defaultParams
。 - 当
manual=true
手动请求模式时,只要ready=false
,则通过run/runAsync
触发的请求都不会执行。
支持依赖刷新,当options中的refreshDeps值变化后,会重新触发请求。
由于这两个需要类似,可以放在一个hook里实现
import { useRef } from 'react';
import useUpdateEffect from '../../../useUpdateEffect';
import type { Plugin } from '../types';
// support refreshDeps & ready
const useAutoRunPlugin: Plugin<any, any[]> = (
fetchInstance,
{ manual, ready = true, defaultParams = [], refreshDeps = [], refreshDepsAction },
) => {
// 避免重复请求的ref。
const hasAutoRun = useRef(false);
hasAutoRun.current = false;
// 在自动模式(manual === false)且ready === true时发送请求。
useUpdateEffect(() => {
if (!manual && ready) {
hasAutoRun.current = true;
fetchInstance.run(...defaultParams);
}
}, [ready]);
// 在自动模式下,当refreshDeps发生变化,会出发refresh事件。
useUpdateEffect(() => {
if (hasAutoRun.current) {
return;
}
if (!manual) {
hasAutoRun.current = true;
fetchInstance.refresh();
}
}, [...refreshDeps]);
return {
// 在请求前判断ready状态,等于true才能发送请求。
onBefore: () => {
if (!ready) {
return {
stopNow: true,
};
}
},
};
};
// 还有一个onInit事件,初始化loading的状态,在自动模式且ready为true时状态才为true。
useAutoRunPlugin.onInit = ({ ready = true, manual }) => {
return {
loading: !manual && ready,
};
};
export default useAutoRunPlugin;
…
其他插件大同小异,可以前往ahooks仓库查看https://github.com/alibaba/hooks/tree/master/packages/hooks/src/useRequest/src/plugins
end.
暂无评论内容