当前位置:网站首页>How to use the plug-in mechanism to gracefully encapsulate your request hook
How to use the plug-in mechanism to gracefully encapsulate your request hook
2022-06-30 12:20:00 【Gopal】
This article is in simple terms ahooks The second article in the source code series , The main goals of this series are as follows :
To deepen the React hooks The understanding of the . Learn how to abstract and customize hooks. Build your own React hooks Tool library . Cultivate the habit of reading and learning source code , Tool library is a good choice for reading source code .
notes : This series is about ahooks The source code analysis of is based on v3.3.13
. own folk A source code , It mainly interprets the source code , so details .
Series articles :
In this article ahooks At the heart of hook —— useRequest.
useRequest brief introduction
According to the official documents ,useRequest Is a powerful asynchronous data management Hooks,React The network request scenario in the project uses useRequest That's enough .
useRequest Organize code through plug-ins , The core code is extremely simple , And it can be easily extended to more advanced functions . The existing capabilities include :
Automatic request / Manual request polling Shake proof throttle Screen focus re request Error retry loading delay SWR(stale-while-revalidate) cache
Here you can see useRequest It's very powerful , If you can do it , How will you achieve it ? You can also see the official answer from the introduction —— Plug in mechanism .
framework
As shown in the figure above , I put the whole useRequest Divided into several modules .
entrance useRequest. It is responsible for initializing the processing data and returning the results . Fetch. As a whole useRequest Core code , It handles the entire request lifecycle . plugin. stay Fetch in , Different plug-in methods will be triggered at different times through the plug-in mechanism , expand useRequest Functional characteristics of . utils and types.ts. Provide tool methods and type definitions .
useRequest Inlet handling
Start with the entry file ,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, [
// Plug in list , Used to expand functions , Not generally used by users . No exposure is seen in the document API
...(plugins || []),
useDebouncePlugin,
useLoadingDelayPlugin,
usePollingPlugin,
useRefreshOnWindowFocusPlugin,
useThrottlePlugin,
useAutoRunPlugin,
useCachePlugin,
useRetryPlugin,
] as Plugin<TData, TParams>[]);
}
export default useRequest;
First here (service Request instance ) The second parameter ( configuration option ), We are familiar with , The third parameter is not mentioned in the document , It's actually a list of plug-ins , Users can customize the plug-in expansion function .
You can see the return useRequestImplement
Method . It's mainly about Fetch Class to instantiate .
const update = useUpdate();
// Ensure that the request instance does not change
const fetchInstance = useCreation(() => {
// At present, only useAutoRunPlugin This plugin There is this method
// Initialization status , return { loading: xxx }, Represents whether loading
const initState = plugins.map((p) => p?.onInit?.(fetchOptions)).filter(Boolean);
// Return request instance
return new Fetch<TData, TParams>(
serviceRef,
fetchOptions,
// Sure useRequestImplement Components
update,
Object.assign({}, ...initState),
);
}, []);
fetchInstance.options = fetchOptions;
// run all plugins hooks
// Carry out all the plugin, Development ability , Every plugin Methods returned by both , Can be executed at a specific time
fetchInstance.pluginImpls = plugins.map((p) => p(fetchInstance, fetchOptions));
When instantiating , The transmission parameters are the request instance in turn ,options Options , Update function of parent component , Initial state value .
One thing to be very careful about here is the last line , It performs all the plugins plug-in unit , What's coming in is fetchInstance Examples and options Options , The returned result is assigned to fetchInstance Example of pluginImpls
.
What this file does is return the result to the developer , I will not elaborate on this point .
Fetch and Plugins
Next, the core source code —— Fetch class . There are not many codes , It's very simple , Let's simplify :
export default class Fetch<TData, TParams extends any[]> {
// The list of methods returned after the plug-in is executed
pluginImpls: PluginReturn<TData, TParams>[];
count: number = 0;
// Several important return values
state: FetchState<TData, TParams> = {
loading: false,
params: undefined,
data: undefined,
error: undefined,
};
constructor(
// React.MutableRefObject —— useRef Type of created , You can modify
public serviceRef: MutableRefObject<Service<TData, TParams>>,
public options: Options<TData, TParams>,
// subscribe - Update function
public subscribe: Subscribe,
// Initial value
public initState: Partial<FetchState<TData, TParams>> = {},
) {
this.state = {
...this.state,
loading: !options.manual, // Non manual , Just loading
...initState,
};
}
// Update status
setState(s: Partial<FetchState<TData, TParams>> = {}) {
this.state = {
...this.state,
...s,
};
this.subscribe();
}
// Execute an event in the plug-in (event),rest Pass in for parameter
runPluginHandler(event: keyof PluginReturn<TData, TParams>, ...rest: any[]) {
// Omit code ...
}
// If set options.manual = true, be useRequest Will not execute by default , Need to pass through run perhaps runAsync To trigger execution .
// runAsync It's a return Promise The asynchronous function of , If you use runAsync To call , That means you need to catch exceptions yourself .
async runAsync(...params: TParams): Promise<TData> {
// Omit code ...
}
// run Is a common synchronization function , It also calls runAsync Method
run(...params: TParams) {
// Omit code ...
}
// Cancel the request currently in progress
cancel() {
// Omit code ...
}
// Use the last params, Call again run
refresh() {
// Omit code ...
}
// Use the last params, Call again runAsync
refreshAsync() {
// Omit code ...
}
// modify data. Parameters can be functions , It can also be a value
mutate(data?: TData | ((oldData?: TData) => TData | undefined)) {
// Omit code ...
}
state as well as setState
stay constructor in , It mainly initializes the data . The data to be maintained mainly includes the following important data as well as the data to be maintained through setState Method to set the data , The setting is completed through subscribe Call notification useRequestImplement Component re render , To get the latest value .
// Several important return values
state: FetchState<TData, TParams> = {
loading: false,
params: undefined,
data: undefined,
error: undefined,
};
// Update status
setState(s: Partial<FetchState<TData, TParams>> = {}) {
this.state = {
...this.state,
...s,
};
this.subscribe();
}
Implementation of plug-in mechanism
As mentioned above, the results of all plug-ins are assigned to pluginImpls. Its type is defined as follows :
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;
}
Except for the last one onMutate outside , You can see that the returned methods are all in the life cycle of a request . A request starts and ends , As shown in the figure below :

If you are more careful , You'll find that Basically all plug-in functions are implemented in one or more phases of a request , That is, we only need to be in the corresponding stage of the request , Execute the logic of our plug-in , We can complete the functions of our plug-ins .
The function to execute the plug-in method at a specific stage is runPluginHandler, Its event The input parameter is the above PluginReturn key value .
// Execute an event in the plug-in (event),rest Pass in for parameter
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);
}
In this way ,Fetch The code of the class will become very compact , You only need to complete the functions of the overall process , All the extra features ( Try again 、 Polling, etc ) It is all left to plug-ins to implement . The advantages of doing this :
Conform to the principle of single responsibility . One Plugin Do one thing , Not related to each other . The overall maintainability is higher , And have better testability . Conform to the software design concept of deep module . It believes that the best modules provide powerful functions , It also has a simple interface . Imagine that each module is represented by a rectangle , Here's the picture , The area of the rectangle is proportional to the function of the module . The top edge represents the interface of the module , The length of the edge represents its complexity . The best modules are deep : They have many functions hidden behind simple interfaces . Deep modules are good abstractions , Because it only exposes a small part of its internal complexity to users .

The core approach —— runAsync
You can see runAsync Is the core method of running requests , Other methods, such as run/refresh/refreshAsync
In the end, the method is called .
And in this method, you can see the life cycle processing of the overall request . This is consistent with the method design returned by the plug-in above .

Before request —— onBefore
Status before processing the request , And implement Plugins Back to onBefore Method , And execute the corresponding logic according to the return value . such as ,useCachePlugin If still stored in fresh time , No request , return returnNow, This will directly return cached data .
this.count += 1;
// Mainly for cancel request
const currentCount = this.count;
const {
stopNow = false,
returnNow = false,
...state
// First, execute the pre function of each plug-in
} = this.runPluginHandler('onBefore', params);
// stop request
if (stopNow) {
return new Promise(() => {});
}
this.setState({
// Start loading
loading: true,
// Request parameters
params,
...state,
});
// return now
// Return immediately , It is related to cache policy
if (returnNow) {
return Promise.resolve(state.data);
}
// onBefore - Trigger before request
// If there is cached data , Then return directly
this.options.onBefore?.(params);
For the request ——onRequest
At this stage, there is only useCachePlugin Yes onRequest Method , Go back to service Promise( It may be the result of caching ), So as to achieve cache Promise The effect of .
// replace service
// If there is cache Example , Use cached instances
let { servicePromise } = this.runPluginHandler('onRequest', this.serviceRef.current, params);
if (!servicePromise) {
servicePromise = this.serviceRef.current(...params);
}
const res = await servicePromise;
useCachePlugin Back to onRequest Method :
// Request stage
onRequest: (service, args) => {
// see promise There is no cache
let servicePromise = cachePromise.getCachePromise(cacheKey);
// If has servicePromise, and is not trigger by self, then use it
// If there is servicePromise, And it is not triggered by itself , Then use it
if (servicePromise && servicePromise !== currentPromiseRef.current) {
return { servicePromise };
}
servicePromise = service(...args);
currentPromiseRef.current = servicePromise;
// Set up promise cache
cachePromise.setCachePromise(cacheKey, servicePromise);
return { servicePromise };
},
Cancel the request —— onCancel
Just before the request started currentCount Variable , In fact, in order to cancel request .
this.count += 1;
// Mainly for cancel request
const currentCount = this.count;
In the course of the request , Developers can call Fetch Of cancel Method :
// Cancel the request currently in progress
cancel() {
// Set up + 1, In execution runAsync When , You will find currentCount !== this.count, So as to achieve the purpose of canceling the request
this.count += 1;
this.setState({
loading: false,
});
// perform plugin All of the onCancel Method
this.runPluginHandler('onCancel');
}
This is the time ,currentCount !== this.count, It will return null data .
// If not the same request , Returns empty promise
if (currentCount !== this.count) {
// prevent run.then when request is canceled
return new Promise(() => {});
}
Final result processing ——onSuccess/onError/onFinally
This part is simple , adopt try...catch... Finally, success , It's right there try Add... At the end onSuccess The logic of , Failure in catch Add... At the end onError The logic of , Both plus onFinally The logic of .
try {
const res = await servicePromise;
// Omit code ...
this.options.onSuccess?.(res, params);
// plugin in onSuccess event
this.runPluginHandler('onSuccess', res, params);
// service Trigger when execution is complete
this.options.onFinally?.(params, res, undefined);
if (currentCount === this.count) {
// plugin in onFinally event
this.runPluginHandler('onFinally', params, res, undefined);
}
return res;
// Capture error
} catch (error) {
// Omit code ...
// service reject Trigger when
this.options.onError?.(error, params);
// perform plugin Medium onError event
this.runPluginHandler('onError', error, params);
// service Trigger when execution is complete
this.options.onFinally?.(params, undefined, error);
if (currentCount === this.count) {
// plugin in onFinally event
this.runPluginHandler('onFinally', params, undefined, error);
}
// Throw an error .
// Let external capture sense errors
throw error;
}
Thinking and summary
useRequest yes ahooks One of the core functions , It's very functional , But the core code (Fetch class ) Relatively simple , This is due to its plug-in mechanism , Give specific functions to specific plug-ins to implement , I am only responsible for the design of the main process , And expose the corresponding execution time .
For our usual components /hook Encapsulation is very helpful , Our abstraction of a complex function , The external interface can be as simple as possible . Internal implementation needs to follow the principle of single responsibility , Through a mechanism similar to plug-in , Refine split components , So as to improve the maintainability of components 、 ...Testability .
Reference resources
边栏推荐
- 90. (cesium chapter) cesium high level listening events
- 各厂家rtsp地址格式如下:
- Openmldb meetup No.4 meeting minutes
- AGCO AI frontier promotion (6.30)
- Embedded sig | multi OS hybrid deployment framework
- Subtrate 源码追新导读-5月上旬: XCM 正式启用
- 1175. prime number arrangement: application of multiplication principle
- 海思3559 sample解析:venc
- 解决服务器重装无法通过ssh连接的问题
- 服务器常用的一些硬件信息(不断更新)
猜你喜欢
立创 EDA #学习笔记10# | 常用连接器元器件识别 和 无源蜂鸣器驱动电路
聊聊怎么做硬件兼容性检测,快速迁移到openEuler?
会议预告 | 华为 2012 实验室全球软件技术峰会-欧洲分会场
lvgl 小部件样式篇
SuperMap iClient3D 11i for Cesium三维场景中图例使用说明
Pinda general permission system (day 7~day 8)
治数如治水,数据治理和数据创新难在哪?
A High-Precision Positioning Approach for Catenary Support Components With Multiscale Difference阅读笔记
使用Power Designer工具构建数据库模型
SuperMap iServer11i新功能----图例的发布和使用
随机推荐
R language ggplot2 visualization: gganimate package is based on Transition_ Time function to create dynamic scatter animation (GIF)
DMA控制器8237A
Map集合
串行通信接口8250
海思3559 sample解析:venc
光谱共焦位移传感器的原理是什么?能应用那些领域?
A new journey of the smart court, paperless office, escorting the green trial of the smart court
拆分电商系统为微服务
MATLAB中polarplot函数使用
lvgl 小部件样式篇
[cf] 803 div2 B. Rising Sand
Introduction to the pursuit of new subtrate source code - early May: xcm officially launched
Swagger2自动生成APi文档
Redis6学习笔记-第二章-Redis6的基本操作
海思3559萬能平臺搭建:獲取數據幀修改後編碼
Redis - SDS simple dynamic string
品达通用权限系统(Day 7~Day 8)
edusoho企培版纯内网部署教程(解决播放器,上传,后台卡顿问题)
21、wpf之绑定使用小记
How can c write an SQL parser