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!!!!!