当前位置: 首页 > Web前端 > JavaScript

如何使用插件机制优雅封装你的请求hook

时间:2023-03-27 18:34:38 JavaScript

本文是深入浅出ahooks源码系列文章的第二篇。本系列已组织成文档地址。我觉得还不错,点个关注支持一下,谢谢。本文讲的是ahooks的核心h??ook——useRequest。useRequest简介根据官方文档的介绍,useRequest是一个强大的异步数据管理Hooks,在React项目中的网络请求场景使用useRequest就足够了。useRequest通过插件的方式组织代码,核心代码极其简单,可以轻松扩展更高级的功能。目前可用的能力包括:自动请求/手动请求轮询防抖节流屏幕焦点重新请求错误重试加载delaySWR(stale-while-revalidate)缓存这里可以看到useRequest的功能很强大,如果你来实现,你将如何实现它?从介绍中也可以看到官方给出的答案——插件机制。架构如上图所示,我把整个useRequest分成了几个模块。条目使用请求。它负责初始化和处理数据并返回结果。拿来。它是整个useRequest的核心代码,处理了整个请求生命周期。插入。Fetch中会通过插件机制在不同的时间触发不同的插件方法,扩展useRequest的功能特性。实用程序和types.ts。提供工具方法和类型定义。useRequest入口处理从入口文件开始,packages/hooks/src/useRequest/src/useRequest.ts。functionuseRequest(service:Service,options?:Options,plugins?:Plugin[],){returnuseRequestImplement(service,options,[//插件列表,用于扩展功能,一般用户不用。文档中没有公开的API...(plugins||[]),useDebouncePlugin,useLoadingDelayPlugin,usePollingPlugin,useRefreshOnWindowFocusPlugin,useThrottlePlugin,useAutoRunPlugin,useCachePlugin,useRetryPlugin,]asPlugin{//目前只有插件useAutoRunPlugin有这个方法//初始化状态,返回{loading:xxx},代表是否加载constinitState=plugins.map((p)=>p?.onInit?.(fetchOptions)).filter(Boolean);//返回请求实例returnnewFetch(serviceRef,fetchOptions,//可以使用Request实现组件更新,Object.assign({},...initState),);},[]);fetchInstance.options=fetchOptions;//runallpluginshooks//执行所有plugins,expandcapabilities,each的插件中返回的方法可以在特定时间执行。fetchInstance.pluginImpls=plugins.map((p)=>p(fetchInstance,fetchOptions));实例化时,传入的参数依次是request实例、options选项、父组件的更新函数、初始状态值。这里需要注意的是最后一行,执行所有的plugins插件,传入fetchInstance实例和options选项,并将返回结果赋值给fetchInstance实例的pluginImpls。另外,这个文件的作用是将结果返回给开发者,这里不详述。Fetch和Plugins的下一个核心源代码部分-Fetch类。它的代码不多,非常精简,先简化一下:exportdefaultclassFetch{//插件执行后返回的方法列表pluginImpls:PluginReturn[];计数:数字=0;//几个重要的返回值state:FetchState={loading:false,params:undefined,data:undefined,error:undefined,};constructor(//React.MutableRefObject——使用Ref创建类型可修改publicserviceRef:MutableRefObject>,publicoptions:Options,//subscription-update函数publicsubscribe:Subscribe,//初始值publicinitState:Partial>={},){this.state={...this.state,loading:!options.manual,//如果不是手动加载...初始状态,};}//更新状态setState(s:Partial>={}){this.state={...this.state,...s,};这个。订阅();}//在插件中执行一些事件(event),rest作为参数传递给runPluginHandler(event:keyofPluginReturn,...rest:any[]){//省略代码...}//如果options.manual=true,那么useRequest默认不会执行,需要要通过run或runAsync来触发执行//runAsync是一个返回Promise的异步函数,如果你使用runAsync来调用,意味着你需要自己捕获异常。asyncrunAsync(...params:TParams):Promise{//省略代码...}//run是一个普通的同步函数,同样会调用runAsync方法run(...params:TParams){//省略代码...}//取消当前请求cancel(){//省略代码...}//使用最后一个参数调用runrefresh(){//省略代码...}//使用最后一个参数,再次调用runAsyncrefreshAsync(){//省略代码...}//修改数据。参数可以是一个函数或者一个值mutate(data?:TData|((oldData?:TData)=>TData|undefined)){//省略代码...}state和setState主要用在构造函数数据初始化。这里维护的数据主要包括以下重要数据,这些数据是通过setState方法设置的。设置完成后,通过subscribe调用通知useRequestImplement组件重新渲染,获取最新值。//几个重要的返回值state:FetchState={loading:false,params:undefined,data:undefined,error:undefined,};//更新状态setState(s:Partial>={}){this.state={...this.state,...s,};this.subscribe();}上面提到的插件机制的实现,就是将所有插件操作的结果都赋值给pluginImpls。它的类型定义如下:exportinterfacePluginReturn{onBefore?:(params:TParams)=>|({stopNow?:boolean;returnNow?:boolean;}&Partial>)|空白;onRequest?:(service:Service,params:TParams,)=>{servicePromise?:Promise;};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,它的事件入参就是上面的PluginReturn键值。//在插件中执行一个事件(event),将rest参数传给runPluginHandler(event:keyofPluginReturn,...rest:any[]){//@ts-ignoreconstr=这个.pluginImpls.map((i)=>i[event]?.(...rest)).filter(Boolean);returnObject.assign({},...r);}这样,Fetch类的代码会变得非常精简,只需要完成整体流程的功能,所有额外的功能(比如重试、轮询等)都交给插件实现。这样做的好处:符合单一职责原则。一个Plugin只做一件事,彼此之间没有关系。整体可维护性更高,具有更好的可测试性。符合深度模块的软件设计理念。它认为最好的模块提供强大的功能和简单的接口。想象一下,每个模块都用一个矩形表示,如下图,矩形的面积与模块实现的功能成正比。顶部边缘代表模块的接口,边缘的长度代表其复杂性。最好的模块是有深度的:它们有很多功能隐藏在一个简单的界面后面。深层模块是一个很好的抽象,因为它只向用户暴露了其内部复杂性的一小部分。核心方法——runAsync可以看到runAsync是运行请求的核心方法,其他方法如run/refresh/refreshAsync最终都会调用这个方法。而在这个方法中,可以看到整个请求生命周期的处理过程。这与上面插件返回的方法设计是一致的。请求前——onBefore处理请求前的状态,执行Plugins返回的onBefore方法,根据返回值执行相应的逻辑。比如useCachePlugin在fresh的时候还保存着,就不需要请求返回returnNow,这样缓存的数据会直接返回。this.count+=1;//主要针对取消请求constcurrentCount=this.count;const{stopNow=false,returnNow=false,...state//先执行各个插件的前端函数}=this.runPluginHandler('onBefore',params);//停止requestif(stopNow){returnnewPromise(()=>{});}this.setState({//开始加载loading:true,//请求参数params,...state,});//returnnow//立即返回,与缓存策略相关直接返回this.options.onBefore?.(params);makearequest——OnRequest该阶段只有useCachePlugin执行onRequest方法,执行后返回一个服务Promise(可能是缓存的结果),从而达到缓存Promise的效果。//替换服务//如果有缓存实例,则使用缓存实例let{servicePromise}=this.runPluginHandler('onRequest',this.serviceRef.current,params);如果(!servicePromise){servicePromise=this.serviceRef.current(...params);}constres=awaitservicePromise;useCachePlugin返回的onRequest方法://请求阶段onRequest:(service,args)=>{//查看promise是否被缓存letservicePromise=cachePromise.getCachePromise(cacheKey);//如果有servicePromise,且不是自己触发,则使用//如果有servicePromise,且不是自己触发,则使用if(servicePromise&&servicePromise!==currentPromiseRef.current){return{servicePromise};}servicePromise=service(...args);currentPromiseRef.current=servicePromise;//设置承诺缓存cachePromise.setCachePromise(cacheKey,servicePromise);return{servicePromise};},取消请求-onCancel刚开始请求之前定义了currentCount变量,但实际上是针对取消请求的。this.count+=1;//主要针对取消请求constcurrentCount=this.count;在请求过程中,开发者可以调用Fetch的cancel方法://取消当前请求cancel(){//set+1.执行runAsync时,会发现currentCount!==this.count,从而达到目的取消请求。这个.count+=1;this.setState({loading:false,});//执行所有插件的onCancel方法this.runPluginHandler('onCancel');}这时候currentCount!==this.count会返回空数据。//如果不是同一个请求,返回一个空的promiseif(currentCount!==this.count){//阻止run.then当请求被取消时returnnewPromise(()=>{});}最终结果处理————onSuccess/onError/onFinally比较简单。try...catch...finally成功后,直接在try最后加上onSuccess的逻辑,如果失败则在catch最后加上onError的逻辑。添加onFinally的逻辑。尝试{constres=awaitservicePromise;//省略代码...this.options.onSuccess?.(res,params);//插件中的onSuccess事件this.runPluginHandler('onSuccess',res,params);//服务触发器this.options.onFinally?.(params,res,undefined);if(currentCount===this.count){//插件中的onFinally事件this.runPluginHandler('onFinally',params,res,undefined);}返回资源;//捕获错误}catch(error){//省略代码...//触发this.options.onError?.(error,params)当服务拒绝时;//在插件中执行onError事件this.runPluginHandler('onError',error,params);//this.options.onFinally在服务执行完成时触发?.(params,undefined,error);if(currentCount===this.count){//onFinally在插件事件中this.runPluginHandler('onFinally',params,undefined,error);}//抛出一个错误。//让外部捕获感知错误throwerror;}思考和结论useRequest是ahooks的核心功能之一。它的功能非常丰富,但是核心代码(Fetch类)比较简单,这要归功于它的插件机制。将特定的功能分配给特定的插件来实现,你只负责设计主流程并暴露相应的执行时序。这对我们平时的组件/钩子封装很有帮助。我们对复杂功能的抽象可以使外部接口尽可能简单。内部实现需要遵循单一职责的原则,通过类似插件的机制,细化和拆分组件,提高组件的可维护性和可测试性。