目录
引言useRequest 简介架构useRequest 入口处理Fetch 和 Plugins
state 以及 setState插件化机制的实现核心方法 —— runAsync请求前 —— onBefore进行请求——onRequest取消请求 —— onCancel最后结果处理——onSuccess/onError/onFinally
思考与总结
引言
本文是深入浅出 ahooks 源码系列文章的第二篇,这个系列的目标主要有以下几点:
加深对 React hooks 的理解。学习如何抽象自定义 hooks。构建属于自己的 React hooks 工具库。培养阅读学习源码的习惯,工具库是一个对源码阅读不错的选择。
注:本系列对 ahooks 的源码解析是基于 v3.3.13。自己 folk 了一份源码,主要是对源码做了一些解读,可见 详情。
系列文章:大家都能看得懂的源码(一)ahooks 整体架构篇
本文来讲下 ahooks 的核心 hook —— useRequest。
useRequest 简介
根据官方文档的介绍,useRequest 是一个强大的异步数据管理的 Hooks,React 项目中的网络请求场景使用 useRequest 就够了。
useRequest 通过插件式组织代码,核心代码极其简单,并且可以很方便的扩展出更高级的功能。目前已有能力包括:
自动请求/手动请求轮询防抖节流屏幕聚焦重新请求错误重试loading delaySWR(stale-while-revalidate)缓存
这里可以看到 useRequest 的功能是非常强大的,如果让你来实现,你会如何实现?也可以从介绍中看到官方的答案——插件化机制。
架构
如上图所示,我把整个 useRequest 分成了几个模块。
入口 useRequest。它负责的是初始化处理数据以及将结果返回。Fetch。是整个 useRequest 的核心代码,它处理了整个请求的生命周期。plugin。在 Fetch 中,会通过插件化机制在不同的时机触发不同的插件方法,拓展 useRequest 的功能特性。utils 和 types.ts。提供工具方法以及类型定义。
useRequest 入口处理
先从入口文件开始,packages/hooks/src/useRequest/src/useRequest.ts。 - function useRequest<TData, TParams extends any[]>(
- service: Service<TData, TParams>,
- options?: Options<TData, TParams>,
- plugins?: Plugin<TData, TParams>[],
- ) {
- return useRequestImplement<TData, TParams>(service, options, [
- // 插件列表,用来拓展功能,一般用户不使用。文档中没有看到暴露 API
- ...(plugins || []),
- useDebouncePlugin,
- useLoadingDelayPlugin,
- usePollingPlugin,
- useRefreshOnWindowFocusPlugin,
- useThrottlePlugin,
- useAutoRunPlugin,
- useCachePlugin,
- useRetryPlugin,
- ] as Plugin<TData, TParams>[]);
- }
- export default useRequest;
复制代码这里第一(service 请求实例)第二个参数(配置选项),我们比较熟悉,第三个参数文档中没有提及,其实就是插件列表,用户可以自定义插件拓展功能。
可以看到返回了 useRequestImplement 方法。主要是对 Fetch 类进行实例化。 - const update = useUpdate();
- // 保证请求实例都不会发生改变
- const fetchInstance = useCreation(() => {
- // 目前只有 useAutoRunPlugin 这个 plugin 有这个方法
- // 初始化状态,返回 { loading: xxx },代表是否 loading
- const initState = plugins.map((p) => p?.onInit?.(fetchOptions)).filter(Boolean);
- // 返回请求实例
- return new Fetch<TData, TParams>(
- serviceRef,
- fetchOptions,
- // 可以 useRequestImplement 组件
- update,
- Object.assign({}, ...initState),
- );
- }, []);
- fetchInstance.options = fetchOptions;
- // run all plugins hooks
- // 执行所有的 plugin,拓展能力,每个 plugin 中都返回的方法,可以在特定时机执行
- fetchInstance.pluginImpls = plugins.map((p) => p(fetchInstance, fetchOptions));
复制代码实例化的时候,传参依次为请求实例,options 选项,父组件的更新函数,初始状态值。
这里需要非常留意的一点是最后一行,它执行了所有的 plugins 插件,传入的是 fetchInstance 实例以及 options 选项,返回的结果赋值给 fetchInstance 实例的 pluginImpls。
另外这个文件做的就是将结果返回给开发者了,这点不细说。
Fetch 和 Plugins
接下来最核心的源码部分 —— Fetch 类。其代码不多,算是非常精简,先简化一下: - export default class Fetch<TData, TParams extends any[]> {
- // 插件执行后返回的方法列表
- pluginImpls: PluginReturn<TData, TParams>[];
- count: number = 0;
- // 几个重要的返回值
- state: FetchState<TData, TParams> = {
- loading: false,
- params: undefined,
- data: undefined,
- error: undefined,
- };
- constructor(
- // React.MutableRefObject —— useRef创建的类型,可以修改
- public serviceRef: MutableRefObject<Service<TData, TParams>>,
- public options: Options<TData, TParams>,
- // 订阅-更新函数
- public subscribe: Subscribe,
- // 初始值
- public initState: Partial<FetchState<TData, TParams>> = {},
- ) {
- this.state = {
- ...this.state,
- loading: !options.manual, // 非手动,就loading
- ...initState,
- };
- }
- // 更新状态
- setState(s: Partial<FetchState<TData, TParams>> = {}) {
- this.state = {
- ...this.state,
- ...s,
- };
- this.subscribe();
- }
- // 执行插件中的某个事件(event),rest 为参数传入
- runPluginHandler(event: keyof PluginReturn<TData, TParams>, ...rest: any[]) {
- // 省略代码...
- }
- // 如果设置了 options.manual = true,则 useRequest 不会默认执行,需要通过 run 或者 runAsync 来触发执行。
- // runAsync 是一个返回 Promise 的异步函数,如果使用 runAsync 来调用,则意味着你需要自己捕获异常。
- async runAsync(...params: TParams): Promise<TData> {
- // 省略代码...
- }
- // run 是一个普通的同步函数,其内部也是调用了 runAsync 方法
- run(...params: TParams) {
- // 省略代码...
- }
- // 取消当前正在进行的请求
- cancel() {
- // 省略代码...
- }
- // 使用上一次的 params,重新调用 run
- refresh() {
- // 省略代码...
- }
- // 使用上一次的 params,重新调用 runAsync
- refreshAsync() {
- // 省略代码...
- }
- // 修改 data。参数可以为函数,也可以是一个值
- mutate(data?: TData | ((oldData?: TData) => TData | undefined)) {
- // 省略代码...
- }
复制代码 state 以及 setState
在 constructor 中,主要是进行了数据的初始化。其中维护的数据主要包含一下几个重要的数据以及通过 setState 方法设置数据,设置完成通过 subscribe 调用通知 useRequestImplement 组件重新渲染,从而获取最新值。 - // 几个重要的返回值
- state: FetchState<TData, TParams> = {
- loading: false,
- params: undefined,
- data: undefined,
- error: undefined,
- };
- // 更新状态
- setState(s: Partial<FetchState<TData, TParams>> = {}) {
- this.state = {
- ...this.state,
- ...s,
- };
- this.subscribe();
- }
复制代码 插件化机制的实现
上文有提到所有的插件运行的结果都赋值给 pluginImpls。它的类型定义如下: - 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;
- }
复制代码除了最后一个 onMutate 之外,可以看到返回的方法都是在一个请求的生命周期中的。一个请求从开始到结束,如下图所示:
如果你比较仔细,你会发现 基本所有的插件功能都是在一个请求的一个或者多个阶段中实现的,也就是说我们只需要在请求的相应阶段,执行我们的插件的逻辑,就能完成我们插件的功能。
执行特定阶段插件方法的函数为 runPluginHandler,其 event 入参就是上面 PluginReturn key 值。 - // 执行插件中的某个事件(event),rest 为参数传入
- 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);
- }
复制代码通过这样的方式,Fetch 类的代码会变得非常的精简,只需要完成整体流程的功能,所有额外的功能(比如重试、轮询等等)都交给插件去实现。这么做的优点:
符合职责单一原则。一个 Plugin 只做一件事,相互之间不相关。整体的可维护性更高,并且拥有更好的可测试性。符合深模块的软件设计理念。其认为最好的模块提供了强大的功能,又有着简单的接口。试想每个模块由一个长方形表示,如下图,长方形的面积大小和模块实现的功能多少成比例。顶部边代表模块的接口,边的长度代表它的复杂度。最好的模块是深的:他们有很多功能隐藏在简单的接口后。深模块是好的抽象,因为它只把自己内部的一小部分复杂度暴露给了用户。
核心方法 —— runAsync
可以看到 runAsync 是运行请求的最核心方法,其他的方法比如 run/refresh/refreshAsync 最终都是调用该方法。
并且该方法中就可以看到整体请求的生命周期的处理。这跟上面插件返回的方法设计是保持一致的。
请求前 —— onBefore
处理请求前的状态,并执行 Plugins 返回的 onBefore 方法,并根据返回值执行相应的逻辑。比如,useCachePlugin 如果还存于新鲜时间内,则不用请求,返回 returnNow,这样就会直接返回缓存的数据。 - this.count += 1;
- // 主要为了 cancel 请求
- const currentCount = this.count;
- const {
- stopNow = false,
- returnNow = false,
- ...state
- // 先执行每个插件的前置函数
- } = this.runPluginHandler('onBefore', params);
- // stop request
- if (stopNow) {
- return new Promise(() => {});
- }
- this.setState({
- // 开始 loading
- loading: true,
- // 请求参数
- params,
- ...state,
- });
- // return now
- // 立即返回,跟缓存策略有关
- if (returnNow) {
- return Promise.resolve(state.data);
- }
- // onBefore - 请求之前触发
- // 假如有缓存数据,则直接返回
- this.options.onBefore?.(params);
复制代码 进行请求——onRequest
这个阶段只有 useCachePlugin 执行了 onRequest 方法,执行后返回 service Promise(有可能是缓存的结果),从而达到缓存 Promise 的效果。 - // replace service
- // 如果有 cache 的实例,则使用缓存的实例
- let { servicePromise } = this.runPluginHandler('onRequest', this.serviceRef.current, params);
- if (!servicePromise) {
- servicePromise = this.serviceRef.current(...params);
- }
- const res = await servicePromise;
复制代码useCachePlugin 返回的 onRequest 方法: - // 请求阶段
- onRequest: (service, args) => {
- // 看 promise 有没有缓存
- let servicePromise = cachePromise.getCachePromise(cacheKey);
- // If has servicePromise, and is not trigger by self, then use it
- // 如果有servicePromise,并且不是自己触发的,那么就使用它
- if (servicePromise && servicePromise !== currentPromiseRef.current) {
- return { servicePromise };
- }
- servicePromise = service(...args);
- currentPromiseRef.current = servicePromise;
- // 设置 promise 缓存
- cachePromise.setCachePromise(cacheKey, servicePromise);
- return { servicePromise };
- },
复制代码 取消请求 —— onCancel
刚刚在请求开始前定义了 currentCount 变量,其实为了 cancel 请求。 - this.count += 1;
- // 主要为了 cancel 请求
- const currentCount = this.count;
复制代码在请求过程中,开发者可以调用 Fetch 的 cancel 方法: - // 取消当前正在进行的请求
- cancel() {
- // 设置 + 1,在执行 runAsync 的时候,就会发现 currentCount !== this.count,从而达到取消请求的目的
- this.count += 1;
- this.setState({
- loading: false,
- });
- // 执行 plugin 中所有的 onCancel 方法
- this.runPluginHandler('onCancel');
- }
复制代码这个时候,currentCount !== this.count,就会返回空数据。 - // 假如不是同一个请求,则返回空的 promise
- if (currentCount !== this.count) {
- // prevent run.then when request is canceled
- return new Promise(() => {});
- }
复制代码 最后结果处理——onSuccess/onError/onFinally
这部分也就比较简单了,通过 try...catch...最后成功,就直接在 try 末尾加上 onSuccess 的逻辑,失败在 catch 末尾加上 onError 的逻辑,两者都加上 onFinally 的逻辑。 - try {
- const res = await servicePromise;
- // 省略代码...
- this.options.onSuccess?.(res, params);
- // plugin 中 onSuccess 事件
- this.runPluginHandler('onSuccess', res, params);
- // service 执行完成时触发
- this.options.onFinally?.(params, res, undefined);
- if (currentCount === this.count) {
- // plugin 中 onFinally 事件
- this.runPluginHandler('onFinally', params, res, undefined);
- }
- return res;
- // 捕获报错
- } catch (error) {
- // 省略代码...
- // service reject 时触发
- this.options.onError?.(error, params);
- // 执行 plugin 中的 onError 事件
- this.runPluginHandler('onError', error, params);
- // service 执行完成时触发
- this.options.onFinally?.(params, undefined, error);
- if (currentCount === this.count) {
- // plugin 中 onFinally 事件
- this.runPluginHandler('onFinally', params, undefined, error);
- }
- // 抛出错误。
- // 让外部捕获感知错误
- throw error;
- }
复制代码 思考与总结
useRequest 是 ahooks 最核心的功能之一,它的功能非常丰富,但核心代码(Fetch 类)相对简单,这得益于它的插件化机制,把特定功能交给特定的插件去实现,自己只负责主流程的设计,并暴露相应的执行时机即可。
这对于我们平时的组件/hook 封装很有帮助,我们对一个复杂功能的抽象,可以尽可能保证对外接口简单。内部实现需要遵循单一职责的原则,通过类似插件化的机制,细化拆分组件,从而提升组件可维护性、可测试性。
参考
软件设计之Deep Module(深模块)
精读 ahooks useRequest 源码
以上就是插件化机制优雅封装你的请求hook使用方式的详细内容,更多关于插件化封装请求hook的资料请关注中国红客联盟其它相关文章! |