二次封装一个请求工具类

1,113 阅读3分钟

二次封装一个请求工具类

契机

在项目开发过程中,除了原生 fetch API,我一直使用两个request 框架:axios 和 阿里的useRequest。

由于项目大多为后台管理系统,自然免不了大量的交互。其中众多交互都需要跟服务端联通,所以就会写大量的接口请求代码,

我将这部分请求代码都写进同一个文件以便管理,直到功能完成进入优化代码阶段,我才会审查原来的代码以做优化,有时候我的

接口文件是这样的

// servers.ts
import { useRequest } from 'ahooks';

const commonHeader = {
  authorization: `bearer ${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,
};

上面的代码是使用 useRequest 处理的,可以看到有不少重复逻辑,所以我打算创建一个函数来帮助我自动写重复部分的代码。

上面的代码中,除了函数名、url 外其余代码都可以复用,我的思路是,创建一个 request 工具函数,我只需要写函数名、请求方式和url就好了,其他都由这个工具函数帮我实现。

思路

根据我的想法,这个工具函数应该是这样用的

// servers.ts 
//这里只需要写请求方式和接口
export default requestBuilder({
  fetchProjectList:"GET /wb/api/projectconfig/query/project/list",
  fetchTenantList:" POST /wb/api/tenant/list/byUserId",
})

// 在 react 中使用,需要实现 useRequest 的绝大部分功能以及 config代码提示,还需要能够支持返回数据类型定义
import api from servers.ts
const Main=()=>{
  const { fetchProjectList,fetchTenantList }=api;
  const data=fetchProjectList({manual: true})  // useRequest手动模式
  const data2=fetchTenantList({formatResult: (res) => res})  //格式化结果
  const data3=fetchTenantList<{data:string[]}>()  //这里需要指定 response 属性中data的类型,以支持代码提示和类型约束
  // ... 能够发请求
  data.run({params:{a:1}})
  data2.run({data:{name:"123"}})
  ...
}
// 发出的请求是这样的
 GET /wb/api/projectconfig/query/project/list?a=1
  
 POST /wb/api/tenant/list/byUserId
  body:{name:"123"}

根据上面的要求,我需要搭建一个基础工具函数,以下是处理逻辑

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 ${localStorage.getItem('access_token')}`,
        },
      }));
  });
  return api;
}

基础框架搭建完毕,后面就是一些细节逻辑的处理,比如TS的类型定义(方便代码提示),比如 useRequest 的传参(这是必须要的)等,目前我优化后能使用在项目中的版本就是这样的:

import { BaseOptions, BaseResult, OptionsWithFormat } from '@ahooksjs/use-request/lib/types';
import { useRequest } from 'ahooks';
import queryString from 'query-string';

const apiPrefix = '/api';//代理模式下的参数
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; // 由于 ahooks 不支持 params 和 data,所以我加了一层逻辑
        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', // 后端协商好传递格式
            tenantId: localStorage.getItem('tenantId'),// 自家业务逻辑
            Authorization: `Bearer ${localStorage.getItem('access_token')}`,//  JWT 方案必传
            ...headers,
          },
          ...rest,
        };
      }, options);
  });
  return api;
}

使用

//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' } });
  }, []);

检查一下,该有的代码提示都是有的。

image-20210720210729560

再看看控制台,理想状态是加载三次请求,并且都具有对应参数

image-20210720211042187

传参都在,说明没啥问题,实际上其他功能也是正常的,可以初步用在项目中,后期随着细节的深入不断完善处理就可以了。

改进

在项目跑几次之后,我又回来更新一下这里的代码,目前项目上的代码是这样的

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 ${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;
  • 增加了一个 startParams 的参数以支持 useRequest 的首次自动加载。
  • 不再使用 commonHeader 这个变量,可能造成变量缓存

封装成类

随着项目的拓展,我发现应该将其封装成一个类,以贴合更多业务场景,比如,我可能会需要判断请求发送成功后,需要给用户做一个提示,像新建成功,编辑成功之类的提示语。这些都是更加项目的场景而自定义的,所以我可以将相关的提示函数封装到这个类中,下次只需要引入一次就可以了。

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; //判断开发、生成环境用的变量

type Servers = {
  [key: string]: <T>(
    startParams?: any,
    options?: BaseOptions<any, any> | OptionsWithFormat<any, any, any, any>,
  ) => BaseResult<any, any>;
};

class HttpTool {
  servers: Servers;
  // 构造函数构造
  constructor(api: { [key: string]: string }) {
    this.servers = HttpTool.initCore(api);
  }
  // init 逻辑
  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;
  }
  // 返回一个useRequest 的包装函数,处理了传参,扩展 data 和 params 选项
  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 ${localStorage.getItem('access_token')}`,
              tenantId: localStorage.getItem('tenantId'),
              ...headers,
              ...startHeaders,
            },
            ...rest,
            ...restParams,
          };
        },
        {
          ...options,
          onSuccess: this.catchFailed,
        },
      );
  }
  //全局失败提示,根据后端返回的结果处理
  static catchFailed(res) {
    if (!res.success) {
      message.error({
        content: res.apiMessage,
        key: 'successHandler',
      });
    }
  }
  //根据处理结果向用户提示内容,可以传入一个回调做下一步动作,也可以 handleSuccess(...).then()做下一步动作
  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()) || '处理成功';
        resolve(callbackResult);
      }
      reject('请求失败');
    });
  }
}
export default HttpTool;

使用

// 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(...)

后话

特别申明: 这个工具函数一定不完美,很多时候好的代码需要经过大量的项目检验以完善其扩展性和易用性,并不能一蹴而就。

因为我们永远预想不到项目会需要用到什么,我们能做的仅仅是尽可能实现功能的同时,把代码优化得更加简洁、易懂。

写这篇博客仅仅只是记录一次对于设计思考、代码改进的过程。如有不足,欢迎留言。

enjoy!!