插件化機制優(yōu)雅封裝你的hook請求使用方式
引言
本文是深入淺出 ahooks 源碼系列文章的第二篇,這個系列的目標主要有以下幾點:
- 加深對 React hooks 的理解。
- 學習如何抽象自定義 hooks。構(gòu)建屬于自己的 React hooks 工具庫。
- 培養(yǎng)閱讀學習源碼的習慣,工具庫是一個對源碼閱讀不錯的選擇。
注:本系列對 ahooks 的源碼解析是基于 v3.3.13。自己 folk 了一份源碼,主要是對源碼做了一些解讀,可見 詳情。
系列文章:大家都能看得懂的源碼(一)ahooks 整體架構(gòu)篇
本文來講下 ahooks 的核心 hook —— useRequest。
useRequest 簡介
根據(jù)官方文檔的介紹,useRequest 是一個強大的異步數(shù)據(jù)管理的 Hooks,React 項目中的網(wǎng)絡(luò)請求場景使用 useRequest 就夠了。
useRequest 通過插件式組織代碼,核心代碼極其簡單,并且可以很方便的擴展出更高級的功能。目前已有能力包括:
- 自動請求/手動請求
- 輪詢
- 防抖
- 節(jié)流
- 屏幕聚焦重新請求
- 錯誤重試
- loading delay
- SWR(stale-while-revalidate)
- 緩存
這里可以看到 useRequest 的功能是非常強大的,如果讓你來實現(xiàn),你會如何實現(xiàn)?也可以從介紹中看到官方的答案——插件化機制。
架構(gòu)

如上圖所示,我把整個 useRequest 分成了幾個模塊。
- 入口 useRequest。它負責的是初始化處理數(shù)據(jù)以及將結(jié)果返回。
- Fetch。是整個 useRequest 的核心代碼,它處理了整個請求的生命周期。
- plugin。在 Fetch 中,會通過插件化機制在不同的時機觸發(fā)不同的插件方法,拓展 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 請求實例)第二個參數(shù)(配置選項),我們比較熟悉,第三個參數(shù)文檔中沒有提及,其實就是插件列表,用戶可以自定義插件拓展功能。
可以看到返回了 useRequestImplement 方法。主要是對 Fetch 類進行實例化。
const update = useUpdate();
// 保證請求實例都不會發(fā)生改變
const fetchInstance = useCreation(() => {
// 目前只有 useAutoRunPlugin 這個 plugin 有這個方法
// 初始化狀態(tài),返回 { 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
// 執(zhí)行所有的 plugin,拓展能力,每個 plugin 中都返回的方法,可以在特定時機執(zhí)行
fetchInstance.pluginImpls = plugins.map((p) => p(fetchInstance, fetchOptions));
實例化的時候,傳參依次為請求實例,options 選項,父組件的更新函數(shù),初始狀態(tài)值。
這里需要非常留意的一點是最后一行,它執(zhí)行了所有的 plugins 插件,傳入的是 fetchInstance 實例以及 options 選項,返回的結(jié)果賦值給 fetchInstance 實例的 pluginImpls。
另外這個文件做的就是將結(jié)果返回給開發(fā)者了,這點不細說。
Fetch 和 Plugins
接下來最核心的源碼部分 —— Fetch 類。其代碼不多,算是非常精簡,先簡化一下:
export default class Fetch<TData, TParams extends any[]> {
// 插件執(zhí)行后返回的方法列表
pluginImpls: PluginReturn<TData, TParams>[];
count: number = 0;
// 幾個重要的返回值
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, // 非手動,就loading
...initState,
};
}
// 更新狀態(tài)
setState(s: Partial<FetchState<TData, TParams>> = {}) {
this.state = {
...this.state,
...s,
};
this.subscribe();
}
// 執(zhí)行插件中的某個事件(event),rest 為參數(shù)傳入
runPluginHandler(event: keyof PluginReturn<TData, TParams>, ...rest: any[]) {
// 省略代碼...
}
// 如果設(shè)置了 options.manual = true,則 useRequest 不會默認執(zhí)行,需要通過 run 或者 runAsync 來觸發(fā)執(zhí)行。
// runAsync 是一個返回 Promise 的異步函數(shù),如果使用 runAsync 來調(diào)用,則意味著你需要自己捕獲異常。
async runAsync(...params: TParams): Promise<TData> {
// 省略代碼...
}
// run 是一個普通的同步函數(shù),其內(nèi)部也是調(diào)用了 runAsync 方法
run(...params: TParams) {
// 省略代碼...
}
// 取消當前正在進行的請求
cancel() {
// 省略代碼...
}
// 使用上一次的 params,重新調(diào)用 run
refresh() {
// 省略代碼...
}
// 使用上一次的 params,重新調(diào)用 runAsync
refreshAsync() {
// 省略代碼...
}
// 修改 data。參數(shù)可以為函數(shù),也可以是一個值
mutate(data?: TData | ((oldData?: TData) => TData | undefined)) {
// 省略代碼...
}
state 以及 setState
在 constructor 中,主要是進行了數(shù)據(jù)的初始化。其中維護的數(shù)據(jù)主要包含一下幾個重要的數(shù)據(jù)以及通過 setState 方法設(shè)置數(shù)據(jù),設(shè)置完成通過 subscribe 調(diào)用通知 useRequestImplement 組件重新渲染,從而獲取最新值。
// 幾個重要的返回值
state: FetchState<TData, TParams> = {
loading: false,
params: undefined,
data: undefined,
error: undefined,
};
// 更新狀態(tài)
setState(s: Partial<FetchState<TData, TParams>> = {}) {
this.state = {
...this.state,
...s,
};
this.subscribe();
}
插件化機制的實現(xià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;
}
除了最后一個 onMutate 之外,可以看到返回的方法都是在一個請求的生命周期中的。一個請求從開始到結(jié)束,如下圖所示:

如果你比較仔細,你會發(fā)現(xiàn)基本所有的插件功能都是在一個請求的一個或者多個階段中實現(xiàn)的,也就是說我們只需要在請求的相應(yīng)階段,執(zhí)行我們的插件的邏輯,就能完成我們插件的功能。
執(zhí)行特定階段插件方法的函數(shù)為 runPluginHandler,其 event 入?yún)⒕褪巧厦?PluginReturn key 值。
// 執(zhí)行插件中的某個事件(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);
}
通過這樣的方式,F(xiàn)etch 類的代碼會變得非常的精簡,只需要完成整體流程的功能,所有額外的功能(比如重試、輪詢等等)都交給插件去實現(xiàn)。這么做的優(yōu)點:
- 符合職責單一原則。一個 Plugin 只做一件事,相互之間不相關(guān)。整體的可維護性更高,并且擁有更好的可測試性。
- 符合深模塊的軟件設(shè)計理念。其認為最好的模塊提供了強大的功能,又有著簡單的接口。試想每個模塊由一個長方形表示,如下圖,長方形的面積大小和模塊實現(xiàn)的功能多少成比例。頂部邊代表模塊的接口,邊的長度代表它的復雜度。最好的模塊是深的:他們有很多功能隱藏在簡單的接口后。深模塊是好的抽象,因為它只把自己內(nèi)部的一小部分復雜度暴露給了用戶。

核心方法 —— runAsync
可以看到 runAsync 是運行請求的最核心方法,其他的方法比如 run/refresh/refreshAsync 最終都是調(diào)用該方法。
并且該方法中就可以看到整體請求的生命周期的處理。這跟上面插件返回的方法設(shè)計是保持一致的。

請求前 —— onBefore
處理請求前的狀態(tài),并執(zhí)行 Plugins 返回的 onBefore 方法,并根據(jù)返回值執(zhí)行相應(yīng)的邏輯。比如,useCachePlugin 如果還存于新鮮時間內(nèi),則不用請求,返回 returnNow,這樣就會直接返回緩存的數(shù)據(jù)。
this.count += 1;
// 主要為了 cancel 請求
const currentCount = this.count;
const {
stopNow = false,
returnNow = false,
...state
// 先執(zhí)行每個插件的前置函數(shù)
} = this.runPluginHandler('onBefore', params);
// stop request
if (stopNow) {
return new Promise(() => {});
}
this.setState({
// 開始 loading
loading: true,
// 請求參數(shù)
params,
...state,
});
// return now
// 立即返回,跟緩存策略有關(guān)
if (returnNow) {
return Promise.resolve(state.data);
}
// onBefore - 請求之前觸發(fā)
// 假如有緩存數(shù)據(jù),則直接返回
this.options.onBefore?.(params);
進行請求——onRequest
這個階段只有 useCachePlugin 執(zhí)行了 onRequest 方法,執(zhí)行后返回 service Promise(有可能是緩存的結(jié)果),從而達到緩存 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,并且不是自己觸發(fā)的,那么就使用它
if (servicePromise && servicePromise !== currentPromiseRef.current) {
return { servicePromise };
}
servicePromise = service(...args);
currentPromiseRef.current = servicePromise;
// 設(shè)置 promise 緩存
cachePromise.setCachePromise(cacheKey, servicePromise);
return { servicePromise };
},
取消請求 —— onCancel
剛剛在請求開始前定義了 currentCount 變量,其實為了 cancel 請求。
this.count += 1; // 主要為了 cancel 請求 const currentCount = this.count;
在請求過程中,開發(fā)者可以調(diào)用 Fetch 的 cancel 方法:
// 取消當前正在進行的請求
cancel() {
// 設(shè)置 + 1,在執(zhí)行 runAsync 的時候,就會發(fā)現(xiàn) currentCount !== this.count,從而達到取消請求的目的
this.count += 1;
this.setState({
loading: false,
});
// 執(zhí)行 plugin 中所有的 onCancel 方法
this.runPluginHandler('onCancel');
}
這個時候,currentCount !== this.count,就會返回空數(shù)據(jù)。
// 假如不是同一個請求,則返回空的 promise
if (currentCount !== this.count) {
// prevent run.then when request is canceled
return new Promise(() => {});
}
最后結(jié)果處理——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 執(zhí)行完成時觸發(fā)
this.options.onFinally?.(params, res, undefined);
if (currentCount === this.count) {
// plugin 中 onFinally 事件
this.runPluginHandler('onFinally', params, res, undefined);
}
return res;
// 捕獲報錯
} catch (error) {
// 省略代碼...
// service reject 時觸發(fā)
this.options.onError?.(error, params);
// 執(zhí)行 plugin 中的 onError 事件
this.runPluginHandler('onError', error, params);
// service 執(zhí)行完成時觸發(fā)
this.options.onFinally?.(params, undefined, error);
if (currentCount === this.count) {
// plugin 中 onFinally 事件
this.runPluginHandler('onFinally', params, undefined, error);
}
// 拋出錯誤。
// 讓外部捕獲感知錯誤
throw error;
}
思考與總結(jié)
useRequest 是 ahooks 最核心的功能之一,它的功能非常豐富,但核心代碼(Fetch 類)相對簡單,這得益于它的插件化機制,把特定功能交給特定的插件去實現(xiàn),自己只負責主流程的設(shè)計,并暴露相應(yīng)的執(zhí)行時機即可。
這對于我們平時的組件/hook 封裝很有幫助,我們對一個復雜功能的抽象,可以盡可能保證對外接口簡單。內(nèi)部實現(xiàn)需要遵循單一職責的原則,通過類似插件化的機制,細化拆分組件,從而提升組件可維護性、可測試性。
參考
以上就是插件化機制優(yōu)雅封裝你的請求hook使用方式的詳細內(nèi)容,更多關(guān)于插件化封裝請求hook的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
如何用webpack4.0擼單頁/多頁腳手架 (jquery, react, vue, typescript)
這篇文章主要介紹了如何用webpack4.0擼單頁/多頁腳手架 (jquery, react, vue, typescript),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-06-06
React?createRef循環(huán)動態(tài)賦值ref問題
這篇文章主要介紹了React?createRef循環(huán)動態(tài)賦值ref問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-01-01
webpack 2.x配置reactjs基本開發(fā)環(huán)境詳解
本篇文章主要介紹了webpack 2.x配置reactjs基本開發(fā)環(huán)境詳解,具有一定的參考價值,有興趣的可以了解一下2017-08-08
React實現(xiàn)文件上傳和斷點續(xù)傳功能的示例代碼
這篇文章主要為大家詳細介紹了React實現(xiàn)文件上傳和斷點續(xù)傳功能的相關(guān)知識,文中的示例代碼講解詳細,感興趣的小伙伴可以跟隨小編一起學習一下2024-02-02

