我們講下 ahooks 的核心 hook —— useRequest。
useRequest 簡(jiǎn)介
根據(jù)官方文檔的介紹,useRequest 是一個(gè)強(qiáng)大的異步數(shù)據(jù)管理的 Hooks,React 項(xiàng)目中的網(wǎng)絡(luò)請(qǐng)求場(chǎng)景使用 useRequest ,這就可以。
useRequest通過(guò)插件式組織代碼,核心代碼極其簡(jiǎn)單,并且可以很方便的擴(kuò)展出更高級(jí)的功能。目前已有能力包括:
自動(dòng)請(qǐng)求/手動(dòng)請(qǐng)求
輪詢
防抖
節(jié)流
屏幕聚焦重新請(qǐng)求
錯(cuò)誤重試
loading delay
SWR(stale-while-revalidate)
緩存
這樣看起來(lái) useRequest 的功能是不是非常的強(qiáng)大,要是會(huì)如何實(shí)現(xiàn)?也可以從介紹中看到官方的答案——插件化機(jī)制。
架構(gòu)
如上圖所示,我把整個(gè) useRequest 分成了幾個(gè)模塊。
入口 useRequest。它負(fù)責(zé)的是初始化處理數(shù)據(jù)以及將結(jié)果返回。
Fetch。是整個(gè) useRequest 的核心代碼,它處理了整個(gè)請(qǐng)求的生命周期。
plugin。在 Fetch 中,會(huì)通過(guò)插件化機(jī)制在不同的時(shí)機(jī)觸發(fā)不同的插件方法,拓展 useRequest 的功能特性。
utils 和 types.ts。提供工具方法以及類型定義。
useRequest 入口處理
先從入口文件開(kāi)始,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, [ // 插件列表,用來(lái)拓展功能,一般用戶不使用。文檔中沒(méi)有看到暴露 API ...(plugins || []), useDebouncePlugin, useLoadingDelayPlugin, usePollingPlugin, useRefreshOnWindowFocusPlugin, useThrottlePlugin, useAutoRunPlugin, useCachePlugin, useRetryPlugin, ] as Plugin<TData, TParams>[]); } export default useRequest;
這里第一(service 請(qǐng)求實(shí)例)第二個(gè)參數(shù)(配置選項(xiàng)),我們比較熟悉,第三個(gè)參數(shù)文檔中沒(méi)有提及,其實(shí)就是插件列表,用戶可以自定義插件拓展功能。
可以看到返回了useRequestImplement方法。主要是對(duì) Fetch 類進(jìn)行實(shí)例化。
const update = useUpdate(); // 保證請(qǐng)求實(shí)例都不會(huì)發(fā)生改變 const fetchInstance = useCreation(() => { // 目前只有 useAutoRunPlugin 這個(gè) plugin 有這個(gè)方法 // 初始化狀態(tài),返回 { loading: xxx },代表是否 loading const initState = plugins.map((p) => p?.onInit?.(fetchOptions)).filter(Boolean); // 返回請(qǐng)求實(shí)例 return new Fetch<TData, TParams>( serviceRef, fetchOptions, // 可以 useRequestImplement 組件 update, Object.assign({}, ...initState), ); }, []); fetchInstance.options = fetchOptions; // run all plugins hooks // 執(zhí)行所有的 plugin,拓展能力,每個(gè) plugin 中都返回的方法,可以在特定時(shí)機(jī)執(zhí)行 fetchInstance.pluginImpls = plugins.map((p) => p(fetchInstance, fetchOptions));
實(shí)例化的時(shí)候,傳參依次為請(qǐng)求實(shí)例,options 選項(xiàng),父組件的更新函數(shù),初始狀態(tài)值。
這里需要非常留意的一點(diǎn)是最后一行,它執(zhí)行了所有的 plugins 插件,傳入的是 fetchInstance 實(shí)例以及 options 選項(xiàng),返回的結(jié)果賦值給 fetchInstance 實(shí)例的pluginImpls。
另外這個(gè)文件做的就是將結(jié)果返回給開(kāi)發(fā)者了,這點(diǎn)不細(xì)說(shuō)。
Fetch 和 Plugins
接下來(lái)最核心的源碼部分 —— Fetch 類。其代碼不多,算是非常精簡(jiǎn),先簡(jiǎn)化一下:
export default class Fetch<TData, TParams extends any[]> { // 插件執(zhí)行后返回的方法列表 pluginImpls: PluginReturn<TData, TParams>[]; count: number = 0; // 幾個(gè)重要的返回值 state: FetchState<TData, TParams> = { loading: false, params: undefined, data: undefined, error: undefined, }; constructor( // React.MutableRefObject —— useRef創(chuàng)建的類型,可以修改 public serviceRef: MutableRefObject<Service<TData, TParams>>, public options: Options<TData, TParams>, // 訂閱-更新函數(shù) public subscribe: Subscribe, // 初始值 public initState: Partial<FetchState<TData, TParams>> = {}, ) { this.state = { ...this.state, loading: !options.manual, // 非手動(dòng),就loading ...initState, }; } // 更新?tīng)顟B(tài) setState(s: Partial<FetchState<TData, TParams>> = {}) { this.state = { ...this.state, ...s, }; this.subscribe(); } // 執(zhí)行插件中的某個(gè)事件(event),rest 為參數(shù)傳入 runPluginHandler(event: keyof PluginReturn<TData, TParams>, ...rest: any[]) { // 省略代碼... } // 如果設(shè)置了 options.manual = true,則 useRequest 不會(huì)默認(rèn)執(zhí)行,需要通過(guò) run 或者 runAsync 來(lái)觸發(fā)執(zhí)行。 // runAsync 是一個(gè)返回 Promise 的異步函數(shù),如果使用 runAsync 來(lái)調(diào)用,則意味著你需要自己捕獲異常。 async runAsync(...params: TParams): Promise<TData> { // 省略代碼... } // run 是一個(gè)普通的同步函數(shù),其內(nèi)部也是調(diào)用了 runAsync 方法 run(...params: TParams) { // 省略代碼... } // 取消當(dāng)前正在進(jìn)行的請(qǐng)求 cancel() { // 省略代碼... } // 使用上一次的 params,重新調(diào)用 run refresh() { // 省略代碼... } // 使用上一次的 params,重新調(diào)用 runAsync refreshAsync() { // 省略代碼... } // 修改 data。參數(shù)可以為函數(shù),也可以是一個(gè)值 mutate(data?: TData | ((oldData?: TData) => TData | undefined)) { // 省略代碼... }
state 以及 setState
在 constructor 中,主要是進(jìn)行了數(shù)據(jù)的初始化。其中維護(hù)的數(shù)據(jù)主要包含一下幾個(gè)重要的數(shù)據(jù)以及通過(guò) setState 方法設(shè)置數(shù)據(jù),設(shè)置完成通過(guò) subscribe 調(diào)用通知 useRequestImplement 組件重新渲染,從而獲取最新值。
// 幾個(gè)重要的返回值 state: FetchState<TData, TParams> = { loading: false, params: undefined, data: undefined, error: undefined, }; // 更新?tīng)顟B(tài) setState(s: Partial<FetchState<TData, TParams>> = {}) { this.state = { ...this.state, ...s, }; this.subscribe(); }
插件化機(jī)制的實(shí)現(xiàn)
上文有提到所有的插件運(yùn)行的結(jié)果都賦值給 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; }
除了最后一個(gè) onMutate 之外,可以看到返回的方法都是在一個(gè)請(qǐng)求的生命周期中的。一個(gè)請(qǐng)求從開(kāi)始到結(jié)束,如下圖所示:
如果你比較仔細(xì),你會(huì)發(fā)現(xiàn)基本所有的插件功能都是在一個(gè)請(qǐng)求的一個(gè)或者多個(gè)階段中實(shí)現(xiàn)的,也就是說(shuō)我們只需要在請(qǐng)求的相應(yīng)階段,執(zhí)行我們的插件的邏輯,就能完成我們插件的功能。
執(zhí)行特定階段插件方法的函數(shù)為 runPluginHandler,其 event 入?yún)⒕褪巧厦?PluginReturn key 值。
// 執(zhí)行插件中的某個(gè)事件(event),rest 為參數(shù)傳入 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); }
通過(guò)這樣的方式,F(xiàn)etch 類的代碼會(huì)變得非常的精簡(jiǎn),只需要完成整體流程的功能,所有額外的功能(比如重試、輪詢等等)都交給插件去實(shí)現(xiàn)。這么做的優(yōu)點(diǎn):
符合職責(zé)單一原則。一個(gè) Plugin 只做一件事,相互之間不相關(guān)。整體的可維護(hù)性更高,并且擁有更好的可測(cè)試性。
符合深模塊的軟件設(shè)計(jì)理念。其認(rèn)為最好的模塊提供了強(qiáng)大的功能,又有著簡(jiǎn)單的接口。試想每個(gè)模塊由一個(gè)長(zhǎng)方形表示,如下圖,長(zhǎng)方形的面積大小和模塊實(shí)現(xiàn)的功能多少成比例。頂部邊代表模塊的接口,邊的長(zhǎng)度代表它的復(fù)雜度。最好的模塊是深的:他們有很多功能隱藏在簡(jiǎn)單的接口后。深模塊是好的抽象,因?yàn)樗话炎约簝?nèi)部的一小部分復(fù)雜度暴露給了用戶。
核心方法 —— runAsync
可以看到 runAsync 是運(yùn)行請(qǐng)求的最核心方法,其他的方法比如run/refresh/refreshAsync最終都是調(diào)用該方法。
并且該方法中就可以看到整體請(qǐng)求的生命周期的處理。這跟上面插件返回的方法設(shè)計(jì)是保持一致的。
請(qǐng)求前 —— onBefore
處理請(qǐng)求前的狀態(tài),并執(zhí)行 Plugins 返回的 onBefore 方法,并根據(jù)返回值執(zhí)行相應(yīng)的邏輯。比如,useCachePlugin 如果還存于新鮮時(shí)間內(nèi),則不用請(qǐng)求,返回 returnNow,這樣就會(huì)直接返回緩存的數(shù)據(jù)。
this.count += 1; // 主要為了 cancel 請(qǐng)求 const currentCount = this.count; const { stopNow = false, returnNow = false, ...state // 先執(zhí)行每個(gè)插件的前置函數(shù) } = this.runPluginHandler('onBefore', params); // stop request if (stopNow) { return new Promise(() => {}); } this.setState({ // 開(kāi)始 loading loading: true, // 請(qǐng)求參數(shù) params, ...state, }); // return now // 立即返回,跟緩存策略有關(guān) if (returnNow) { return Promise.resolve(state.data); } // onBefore - 請(qǐng)求之前觸發(fā) // 假如有緩存數(shù)據(jù),則直接返回 this.options.onBefore?.(params);
進(jìn)行請(qǐng)求——onRequest
這個(gè)階段只有 useCachePlugin 執(zhí)行了 onRequest 方法,執(zhí)行后返回 service Promise(有可能是緩存的結(jié)果),從而達(dá)到緩存 Promise 的效果。
// replace service // 如果有 cache 的實(shí)例,則使用緩存的實(shí)例 let { servicePromise } = this.runPluginHandler('onRequest', this.serviceRef.current, params); if (!servicePromise) { servicePromise = this.serviceRef.current(...params); } const res = await servicePromise;
useCachePlugin 返回的 onRequest 方法:
// 請(qǐng)求階段 onRequest: (service, args) => { // 看 promise 有沒(méi)有緩存 let servicePromise = cachePromise.getCachePromise(cacheKey); // If has servicePromise, and is not trigger by self, then use it // 如果有servicePromise,并且不是自己觸發(fā)的,那么就使用它 if (servicePromise && servicePromise !== currentPromiseRef.current) { return { servicePromise }; } servicePromise = service(...args); currentPromiseRef.current = servicePromise; // 設(shè)置 promise 緩存 cachePromise.setCachePromise(cacheKey, servicePromise); return { servicePromise }; },
取消請(qǐng)求 —— onCancel
剛剛在請(qǐng)求開(kāi)始前定義了 currentCount 變量,其實(shí)為了 cancel 請(qǐng)求。
this.count += 1; // 主要為了 cancel 請(qǐng)求 const currentCount = this.count;
在請(qǐng)求過(guò)程中,開(kāi)發(fā)者可以調(diào)用 Fetch 的 cancel 方法:
// 取消當(dāng)前正在進(jìn)行的請(qǐng)求 cancel() { // 設(shè)置 + 1,在執(zhí)行 runAsync 的時(shí)候,就會(huì)發(fā)現(xiàn) currentCount !== this.count,從而達(dá)到取消請(qǐng)求的目的 this.count += 1; this.setState({ loading: false, }); // 執(zhí)行 plugin 中所有的 onCancel 方法 this.runPluginHandler('onCancel'); }
這個(gè)時(shí)候,currentCount !== this.count,就會(huì)返回空數(shù)據(jù)。
// 假如不是同一個(gè)請(qǐng)求,則返回空的 promise if (currentCount !== this.count) { // prevent run.then when request is canceled return new Promise(() => {}); }
最后結(jié)果處理——onSuccess/onError/onFinally
這部分也就比較簡(jiǎn)單了,通過(guò) 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 執(zhí)行完成時(shí)觸發(fā) this.options.onFinally?.(params, res, undefined); if (currentCount === this.count) { // plugin 中 onFinally 事件 this.runPluginHandler('onFinally', params, res, undefined); } return res; // 捕獲報(bào)錯(cuò) } catch (error) { // 省略代碼... // service reject 時(shí)觸發(fā) this.options.onError?.(error, params); // 執(zhí)行 plugin 中的 onError 事件 this.runPluginHandler('onError', error, params); // service 執(zhí)行完成時(shí)觸發(fā) this.options.onFinally?.(params, undefined, error); if (currentCount === this.count) { // plugin 中 onFinally 事件 this.runPluginHandler('onFinally', params, undefined, error); } // 拋出錯(cuò)誤。 // 讓外部捕獲感知錯(cuò)誤 throw error; }
思考與總結(jié)
useRequest 是 ahooks 最核心的功能之一,它的功能非常豐富,可核心代碼(Fetch 類)卻很簡(jiǎn)便,這就是插件化機(jī)制優(yōu)勢(shì),可以把特定功能交給特定的插件去實(shí)現(xiàn),這樣就只需負(fù)責(zé)主流程的設(shè)計(jì),并暴露相應(yīng)的執(zhí)行時(shí)機(jī)即可。
組件/hook 封裝真的作用十分大,當(dāng)我們面對(duì)對(duì)一個(gè)復(fù)雜功能的抽象,可以盡可能保證對(duì)外接口簡(jiǎn)單。內(nèi)部實(shí)現(xiàn)需要遵循單一職責(zé)的原則,通過(guò)類似插件化的機(jī)制,細(xì)化拆分組件,從而提升組件可維護(hù)性、可測(cè)試性。
希望大家都可以好好學(xué)習(xí),多多實(shí)踐操作。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/128266.html
大家會(huì)發(fā)現(xiàn),自從 React v16.8 推出了 Hooks API,前端框架圈并開(kāi)啟了新的邏輯復(fù)用的時(shí)代,從此無(wú)需在意 HOC 的無(wú)限套娃導(dǎo)致性能差的問(wèn)題,同時(shí)也解決了 mixin 的可閱讀性差的問(wèn)題。這里也有對(duì)于 React 最大的變化是函數(shù)式組件可以有自己的狀態(tài),扁平化的邏輯組織方式,更加友好地支持 TS 類型聲明。 在運(yùn)用Hooks的時(shí)候,除了 React 官方提供的,同時(shí)也支持我們...
摘要:什么樣的對(duì)象容易找到靜態(tài)變量和單例。在一個(gè)進(jìn)程之內(nèi),靜態(tài)變量和單例變量是相對(duì)不容易發(fā)生變化的,因此非常容易定位,而普通的對(duì)象則要么無(wú)法標(biāo)志,要么容易改變。 前言 為了實(shí)現(xiàn) App 的快速迭代更新,基于 H5 Hybrid 的解決方案有很多,由于 webview 本身的性能問(wèn)題,也隨之出現(xiàn)了很多基于 JS 引擎實(shí)現(xiàn)的原生渲染的方案,例如 React Native、weex 等,而國(guó)內(nèi)一線...
摘要:執(zhí)行完成后會(huì)返回如下圖的結(jié)果,根據(jù)返回?cái)?shù)據(jù)把源碼和存儲(chǔ)在的屬性上的回調(diào)函數(shù)中調(diào)用類生成,并根據(jù)生成依賴后回調(diào)方法返回類。 webpack設(shè)計(jì)模式 一切資源皆Module Module(模塊)是webpack的中的關(guān)鍵實(shí)體。Webpack 會(huì)從配置的 Entry 開(kāi)始遞歸找出所有依賴的模塊. 通過(guò)Loaders(模塊轉(zhuǎn)換器),用于把模塊原內(nèi)容按照需求轉(zhuǎn)換成新模塊內(nèi)容. 事件驅(qū)動(dòng)架構(gòu) we...
閱讀 547·2023-03-27 18:33
閱讀 732·2023-03-26 17:27
閱讀 630·2023-03-26 17:14
閱讀 591·2023-03-17 21:13
閱讀 521·2023-03-17 08:28
閱讀 1801·2023-02-27 22:32
閱讀 1292·2023-02-27 22:27
閱讀 2178·2023-01-20 08:28