Rewrap a request utility class
An opportunity to
During the development of the project, in addition to the native FETCH API, I have been using two Request frameworks: Axios and Ali’s useRequest.
Since most projects are back-office management systems, a lot of interaction is inevitable. Many of these interactions need to be connected to the server, so a lot of interface request code is written,
I put this part of the request code in the same file for management, until the function is finished and the optimization code phase, I will review the original code to optimize, sometimes my
The interface file looks like this
// servers.ts
import { useRequest } from 'ahooks';
const commonHeader = {
authorization: `bearer The ${localStorage.getItem('access_token')}`};const fetchTenantList = () = > {
return useRequest<{ data: Home.TenantList[] }>(() = > ({
url: `/wb/api/tenant/list/byUserId`.headers: {
...commonHeader,
},
}));
};
const fetchProjectList = () = > {
return useRequest<{ data: Home.ProjectList }>(() = > ({
url: `/wb/api/projectconfig/query/project/list? pageable=1&size=3`.headers: {
...commonHeader,
},
}));
};
export default {
fetchProjectList,
fetchTenantList,
};
Copy the code
The above code is handled using useRequest and you can see that there is quite a bit of repeating logic, so I’m going to create a function to help me automatically write the repeating parts of the code.
In the above code, except for the function name, URL, the rest of the code can be reused, my idea is to create a request tool function, I only need to write the function name, request method and URL, the other functions are implemented by this tool function for me.
Train of thought
In my opinion, this is how the utility function should be used
// servers.ts
// Just write the request mode and interface
export default requestBuilder({
fetchProjectList:"GET /wb/api/projectconfig/query/project/list".fetchTenantList:" POST /wb/api/tenant/list/byUserId",})// To use react, you need to implement most of the functions of useRequest and config code prompts. You also need to be able to return data type definitions
import api from servers.ts
const Main=() = >{
const { fetchProjectList,fetchTenantList }=api;
const data=fetchProjectList({manual: true}) // useRequest manual mode
const data2=fetchTenantList({formatResult: (res) = > res}) // Format the result
const data3=fetchTenantList<{data:string[]}>() // We need to specify the type of data in the response property to support code hints and type constraints
/ /... Being able to make requests
data.run({params: {a:1}})
data2.run({data: {name:"123"}})... }// The request is like thisGET /wb/api/projectconfig/query/project/list? a=1
POST /wb/api/tenant/list/byUserId
body: {name:"123"}
Copy the code
According to the above requirements, I need to build a basic tool function, the following is the processing logic
function requestBuilder(fetchObj) {
let api = {};
Object.keys(fetchObj).map((item) = > {
const [method, url] = fetchObj[item].trim().replace(/\s+/g.', ').split(', ');
api[item] = () = >
useRequest(() = > ({
url: `${apiPrefix}${url}`,
method,
headers: {
authorization: `bearer The ${localStorage.getItem('access_token')}`,}})); });return api;
}
Copy the code
After the basic framework is set up, there are some details of logic processing, such as TS type definition (convenient code prompt), such as useRequest parameter transmission (which is a must), etc. At present, the version I can use in the project after optimization is like this:
import { BaseOptions, BaseResult, OptionsWithFormat } from '@ahooksjs/use-request/lib/types';
import { useRequest } from 'ahooks';
import queryString from 'query-string';
const apiPrefix = '/api';// Proxy mode parameters
type Api = {
[key: string]: <T>(options? : BaseOptions<any.any> | OptionsWithFormat<any.any.any.any>,
) = > BaseResult<any.any>;
};
function requestBuilder(fetchObj: { [key: string] :string }) :Api {
let api: Api = {};
Object.keys(fetchObj).forEach((item) = > {
const [method, url] = fetchObj[item].trim().replace(/\s+/g.', ').split(', ');
api[item] = <T>(options = {}) = >
useRequest<T>((runParams = {}) = > {
const{ headers, params, data, ... rest } = runParams;// Since ahooks don't support params and data, I added a layer of logic
let query = ' ';
if (params) {
query = `?${queryString.stringify(params, {
skipEmptyString: true,
skipNull: true})},`;
}
return {
url: `${apiPrefix}${url}${query}`,
method,
body: data ? JSON.stringify(data) : null.headers: {
'Content-Type': 'application/json; charset=utf-8'.Accept: 'application/json'.// The backend negotiates the delivery format
tenantId: localStorage.getItem('tenantId'),// Own business logic
Authorization: `Bearer The ${localStorage.getItem('access_token')}`.// The JWT scheme must be passed
...headers,
},
...rest,
};
}, options);
});
return api;
}
Copy the code
use
//servers.ts
export default requestBuilder({
fetchA: 'GET /aaaa'.fetchB: 'POST /bbbbb'});// index.tsx
const { fetchA, fetchB } = api;
const a = fetchA({ manual: true });
const b = fetchB({ formatResult: (res) = > res });
useEffect(() = > {
a.run({ params: { a: 1}}); b.run({data: { name: '123'}}); } []);Copy the code
Check that all the code hints are there.
Looking at the console, the ideal situation is to load three requests, all with corresponding parameters
In fact, other functions are normal, which can be initially used in the project. In the later stage, it is ok to continue to improve the processing as details go deeper.
To improve the
After running the project a few times, I came back to update the code here, which currently looks like this on the project
import { useRequest } from 'ahooks';
import api from '@/utils/config.js';
import { BaseOptions, BaseResult, OptionsWithFormat } from '@ahooksjs/use-request/lib/types';
import queryString from 'query-string';
import { message } from 'antd';
const { apiPrefix } = api;
type Api = {
[key: string]: <T>(startParams? :any, options? : BaseOptions<any.any> | OptionsWithFormat<any.any.any.any>,
) = > BaseResult<any.any>;
};
function requestBuilder(fetchObj: { [key: string] :string }) :Api {
let api: Api = {};
Object.keys(fetchObj).forEach((item) = > {
const [method, url] = fetchObj[item].trim().replace(/\s+/g.', ').split(', ');
api[item] = <T>(startParams: any = {},options = {} ) = >
useRequest<T>(
(runParams = {}) = > {
const {
headers: startHeaders,
params: startQuery,
data: startData, ... restParams } = startParams;const{ headers, params, data, ... rest } = runParams;let query = ' ';
if (params || startQuery) {
query = `?${queryString.stringify(params || startQuery, {
skipEmptyString: true,
skipNull: true})},`;
}
return {
url: `${apiPrefix}${url}${query}`,
method,
body: data || startData ? JSON.stringify(data || startData) : null.headers: {
'Content-Type': 'application/json; charset=utf-8'.Accept: 'application/json'.Authorization: `Bearer The ${localStorage.getItem('access_token')}`.tenantId: localStorage.getItem('tenantId'),... headers, ... startHeaders, }, ... rest, ... restParams, }; }, {... options,onSuccess: (res) = > {
if(! res.success) { message.error({content: res.apiMessage,
key: 'successHandler'}); }}},); });return api;
}
export default requestBuilder;
Copy the code
- A startParams parameter was added to support the first automatic loading of useRequest.
- Not using the commonHeader variable may cause variable caching
Encapsulated into a class
As the project expanded, I found that I should encapsulate it into a class to fit more business scenarios. For example, I might need to tell the user that a request was successfully sent and that it was successfully created or edited. These are all custom for more project scenarios, so I can wrap the relevant prompt functions into this class and only need to introduce them once next time.
import { useRequest } from 'ahooks';
import config from '@/utils/config.js';
import { BaseOptions, BaseResult, OptionsWithFormat } from '@ahooksjs/use-request/lib/types';
import queryString from 'query-string';
import { message } from 'antd';
const { apiPrefix } = config; // Determine the variables used in the development and build environment
type Servers = {
[key: string]: <T>(startParams? :any, options? : BaseOptions<any.any> | OptionsWithFormat<any.any.any.any>,
) = > BaseResult<any.any>;
};
class HttpTool {
servers: Servers;
// Constructor constructor
constructor(api: { [key: string] :string }) {
this.servers = HttpTool.initCore(api);
}
/ / init logic
static initCore(api: { [key: string] :string }) {
const _servers: Servers = {};
Object.keys(api).forEach((item) = > {
const [method, url] = api[item].trim().replace(/\s+/g.', ').split(', ');
_servers[item] = this.createRequest(method, url);
});
return _servers;
}
// Returns a wrapper function for useRequest, which handles pass-throughs and extends the data and params options
static createRequest(url: string, method: string) {
return <T>(startParams: any = {}, options = {}) = >
useRequest<T>(
(runParams = {}) = > {
const {
headers: startHeaders,
params: startQuery,
data: startData, ... restParams } = startParams;const{ headers, params, data, ... rest } = runParams;let query = ' ';
if (params || startQuery) {
query = `?${queryString.stringify(params || startQuery, {
skipEmptyString: true,
skipNull: true})},`;
}
return {
url: `${apiPrefix}${url}${query}`,
method,
body: data || startData ? JSON.stringify(data || startData) : null.headers: {
'Content-Type': 'application/json; charset=utf-8'.Accept: 'application/json'.Authorization: `Bearer The ${localStorage.getItem('access_token')}`.tenantId: localStorage.getItem('tenantId'),... headers, ... startHeaders, }, ... rest, ... restParams, }; }, {... options,onSuccess: this.catchFailed,
},
);
}
// Global failure message, according to the result returned by the backend
static catchFailed(res) {
if(! res.success) { message.error({content: res.apiMessage,
key: 'successHandler'}); }}// Based on the result of the process, you can either pass in a callback for the next action, or handleSuccess(...). .then() does the next action
handleSuccess(res: any, content: string, callback? : (... rest) =>any) {
return new Promise<void> ((resolve, reject) = > {
if (res.success) {
message.success({
key: 'handleSuccess'.content: content,
});
const callbackResult = (callback && callback()) || 'Processing successful';
resolve(callbackResult);
}
reject('Request failed'); }); }}export default HttpTool;
Copy the code
use
// servers.ts
import HttpTool from './HttpTool.ts'
httpApi=new HttpTool({
fetchName:'GET /wb/get/name'
})
export httpApi
// index.tsx
import httpApi from './servers.ts'
const { fetchName }=httpApi.servers
const name=fetchName(...)
httpApi.handleSuccess(...)
Copy the code
The latter
Special note: this tool is not perfect, many times good code needs to go through a lot of project testing to improve its extensibility and ease of use, not overnight.
Because we can never predict what the project will need, we can only optimize the code to be more concise and understandable while making it as functional as possible.
Writing this blog is just to record a process of design thinking and code improvement. If you have any questions, please leave a message.
Enjoy!!!!!