项目实践对 axios 和 umiRequest 请求的封装📦

654 阅读2分钟

「这是我参与2022首次更文挑战的第34天,活动详情查看:2022首次更文挑战

基于 axios 的封装

// axios实例
export const createInstatnce = ({
  baseURL = '',
  timeout = 30000,
  headers = {}
} = {}) => {
  const instance = axios.create({
    baseURL,
    timeout,
    headers: {
      'X-Requested-With': 'XMLHttpRequest',
      'Content-Type': 'application/json',
      headers
    }
  });
  return instance;
};

通用的 get 和 post 生成器

export const commonGetGenerator = instance => {
  return async (url, params = {}, options = {}) =>
    instance
      .get(url, {
        params,
        ...options
      })
      .then(res => res.data);
};

export const commonPostGenerator = instance => {
  return async (url, data, options) =>
    instance.post(url, data, options).then(res => res.data);
};

拦截器

instance.interceptors.request.use(
  config => {
    // 自定义请求头
    config.headers.common.Id = String(Math.random()).slice(2);
    return {
      ...config,
      data: config.data || {}
    };
  },
  error => {
    console.error('request error : ', error);
    return Promise.reject(error);
  }
);

instance.interceptors.response.use(
  res => {
    const {
      data,
      config: { url }
    } = res;
    // code非0业务逻辑处理
    if (data && data.code) { }
    return res;
  },
  error => {
    notification.error({
      message: `${error.config.url}:请求发生错误`,
      description: `${error.message}\n ${error.stack} `
    });
    console.error('请求异常 : ', error);
    return Promise.reject(error);
  }
);

创建 get 和 post 请求

export const get = commonGetGenerator(instance);
export const post = commonPostGenerator(instance);

使用

post('/api/url', param)
  .then(({ data = [] }) => {
    setLoading(false);
    setList(data);
  })
  .catch(err => {
    setLoading(false);
  });

取消重复请求

对请求进行“防抖”处理。这里主要是为了阻止用户在某些情况下短时间内重复点击某个按钮,导致前端向后端重复发送多次请求。

import axios from 'axios';

/* 创建axios实例 */
const service = axios.create({
  baseURL: 'http://example.com/',
  timeout: 30000, // 请求超时时间
  withCredentials: true
});

debounce 配置项,防抖

/* request拦截器 */
service.interceptors.request.use(
  (config) => {
    if (config.url && /^https?:\/\//.test(config.url)) {
      config.baseURL = '';
    }
    // debounce 配置项,防抖
    if (config.debounce) {
      // 生成cancelToken
      config.cancelToken = new CancelToken((handleCancel) => {
        removePending(config, handleCancel);
      });
    }

    config.headers = !(() => {
      let custom_headers = {
        credentials: 'include'
      };
      if (config.data instanceof FormData) {
        custom_headers['Content-Type'] = 'multipart/form-data';
      }
      return {
        ...custom_headers,
        ...config.headers
      };
    });

    // 在这里可以做一些埋点的额外工作
    return config;
  },
  (error: any) => {
    Promise.reject(error);
  }
);
/* respone拦截器 */
service.interceptors.response.use(
  (response) => {
    // 移除队列中的该请求
    removePending(response.config);
    const res = response.data;
    if (response.status >= 400) {
      throw new Error(
        JSON.stringify({
          code: response.status,
          message: response.statusText
        })
      );
    }
    if (res.errcode || res.errno) {
      // 错误处理
    }
    return Promise.resolve(res);
  },
  (error: any) => {
    // 异常处理
    if (error.message === '取消重复请求') {
      return Promise.reject(error);
    }
    return Promise.reject(error);
  }
);

/* 防止重复提交,利用axios的cancelToken */
let pending: string[] = []; // 声明一个数组用于存储每个ajax请求的取消函数和ajax标识
const CancelToken = axios.CancelToken;

const removePending = (config, handleCancel) => {
  // 获取请求的url
  const flagUrl = config.url;
  // 判断该请求是否在请求队列中
  if (flagUrl) {
    if (pending.indexOf(flagUrl) !== -1) {
      // 如果在请求中,并存在f,f即axios提供的取消函数
      if (handleCancel) {
        handleCancel('取消重复请求'); // 执行取消操作
      } else {
        pending.splice(pending.indexOf(flagUrl), 1); // 把这条记录从数组中移除
      }
    } else {
      // 如果不存在在请求队列中,加入队列
      if (handleCancel) {
        pending.push(flagUrl);
      }
    }
  }
};

使用泛型对返回数据类型进行了修饰

export function get<T>(
  url: string,
  params?: any,
  options?: IHttpOption
): AxiosPromise<T> {
  return service({
    ...{ options },
    url,
    method: 'GET',
    params
  });
}

使用

export const getList = (ids: Array<number>) => {
  return get<{
    list: ListType[];
  }>('/api/url/', { ids });
};

基于 umiRequest 封装

对adaptor、timeout和headers进行了处理

import _ from 'lodash';
import { RequestConfig } from 'umi';

export const request: RequestConfig = {
  timeout: 30000,
  // 请求拦截
  requestInterceptors: [
    (url, options) => {
      // 不包含 get 请求
      if (
        _.includes(['post', 'put'], _.lowerCase(options.method)) &&
        options.requestType !== 'form' &&
        !_.get(options, 'headers.Content-Type')
      ) {
        // 向后端提交,添加 application/json
        _.set(options, 'headers.Content-Type', 'application/json');
      }
      return { url, options };
    },
  ],
  errorConfig: {
    adaptor: (res) => {
      return {
        ...res,
        success: res?.error === 0 || res?.code === 200,
        errorMessage: res?.errorMessage || res?.msg || '接口出错',
      };
    },
  },
};

request封装

export const request = (
  url: string,
  options: iKey<any>,
  otherUrlHeader?: string,
) => umiRequest(`${otherUrlHeader || PrefixApiUri}${url}`, options);

使用

export const ajax1 = (data: DataType) => {
  return request(`${BaseUrl}/api/url`, {
    method: 'GET',
    params: data,
  });
};

export const ajax2 = (id: string) => {
  return request(`${BaseUrl}/api/url/api`, {
    method: 'POST',
    data: {
      get_id: `${id}`,
    },
  });
};

对于传递的数据,需要做一些额外的处理。

import _ from 'lodash';
import { RequestConfig } from 'umi';
import moment from "moment";
import { message } from "antd";

export const request: RequestConfig = {
  timeout: 30000,
  requestInterceptors: [
    (url, options) => {
      // 未设置content-type情况下,重置post
      if (
        _.includes(["post", "put"], _.lowerCase(options.method)) &&
        options.requestType !== "form" &&
        !_.get(options, "headers.Content-Type")
      ) {
        _.set(options, "headers.Content-Type", "application/json");
      }
      return { url, options };
    },
  ],
};

提交数据一般都是使用 post 方法。

// 封装 post 方法
export const post = (url: string, data?: any) => {
  if (isObject(data) || Array.isArray(data)) {
    return request(url, {
      method: "post",
      data: formatValues(data),
    }).then((responseData) => {
      const { error, errMessage } = responseData || {};
      if (error !== 0) {
        message.error(errMessage);
      }
      return responseData;
    });
  } else {
    return request(url, { method: "post" }).then((responseData) => {
      const { error, errMessage } = responseData || {};
      if (error !== 0) {
        message.error(errMessage);
      }
      return responseData;
    });
  }
};

formatValues

const isObject = (data: any): boolean => Object.prototype.toString.call(data) === "[object Object]";

const isString = (data: any): boolean => Object.prototype.toString.call(data) === "[object String]";

const isFalsyArray = (data: any[]) => {
  if (Array.isArray(data)) {
    return data.every((item) => item === null || item === "" || item === undefined);
  }
  return false;
};

export const formatValues = (data: any) => {
  const newData = _.cloneDeep(data);

  const formatDateTime = (data: any) => {
    for (const [key, value] of Object.entries(data)) {
      if (isString(value)) {
        data[key] = (value as string).trim();
      } else if (value instanceof moment) {
        data[key] = moment(value).format('YYYY-MM-DD HH:mm:ss');
      } else {
        if (Array.isArray(value) || isObject(value)) {
          formatDateTime(value);
        }
      }
      //trim之后为空字符串的话就不要传给后端了
      if (data[key] === "" || data[key] === null || isFalsyArray(data[key])) {
        data[key] = undefined;
      }
    }
  };

  // 格式化时间数据,并且忽略''值和null
  formatDateTime(newData);
  return newData;
};

使用

post("/api/url", { id: 1 }).then((res: { data: DataType }) => { });