插件化封装hook请求的实现及过程是什么
Admin 2022-08-10 群英技术资讯 279 次浏览
根据官方文档的介绍,useRequest 是一个强大的异步数据管理的 Hooks,React 项目中的网络请求场景使用 useRequest 就够了。
useRequest 通过插件式组织代码,核心代码极其简单,并且可以很方便的扩展出更高级的功能。目前已有能力包括:
这里可以看到 useRequest 的功能是非常强大的,如果让你来实现,你会如何实现?也可以从介绍中看到官方的答案——插件化机制。
如上图所示,我把整个 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 类。其代码不多,算是非常精简,先简化一下:
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)) { // 省略代码... }
在 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 类的代码会变得非常的精简,只需要完成整体流程的功能,所有额外的功能(比如重试、轮询等等)都交给插件去实现。这么做的优点:
可以看到 runAsync 是运行请求的最核心方法,其他的方法比如 run/refresh/refreshAsync
最终都是调用该方法。
并且该方法中就可以看到整体请求的生命周期的处理。这跟上面插件返回的方法设计是保持一致的。
处理请求前的状态,并执行 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);
这个阶段只有 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 }; },
刚刚在请求开始前定义了 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(() => {}); }
这部分也就比较简单了,通过 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 封装很有帮助,我们对一个复杂功能的抽象,可以尽可能保证对外接口简单。内部实现需要遵循单一职责的原则,通过类似插件化的机制,细化拆分组件,从而提升组件可维护性、可测试性。
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:mmqy2019@163.com进行举报,并提供相关证据,查实之后,将立刻删除涉嫌侵权内容。
猜你喜欢
前端开发人员只要了解过vue.js框架可能都知道单文件组件。vue.js中的单文件组件允许在一个文件中定义一个组件的所有内容。这是一个非常有用的解决方案,在浏览器网页中已经开始提倡这种机制。
这篇文章主要为大家详细介绍了js实现简单图片切换,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
这篇文章给大家分享的是JS实现获取文本框的内容的方法。小编觉得挺实用的,对新手学习JavaScript的使用有帮助,因此分享给大家做个参考,文中的示例代码介绍得很详细,有需要的朋友可以参考,接下来就跟随小编一起了解看看吧。
在之前的文章中,我们带大家学习了JS中的几种循环控制结构(while和do-while循环、for循环),下面聊聊跳出循环语句break和continue,希望对大家有所帮助!
这篇文章主要为大家详细介绍了JS实现轮播图效果的3种简单方法,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
成为群英会员,开启智能安全云计算之旅
立即注册Copyright © QY Network Company Ltd. All Rights Reserved. 2003-2020 群英 版权所有
增值电信经营许可证 : B1.B2-20140078 粤ICP备09006778号 域名注册商资质 粤 D3.1-20240008