简单点对Axios和vue-request进行整合封装

978 阅读4分钟

一直在想如何封装axios才是最合适

  1. 保留原有库(axios)设计
  2. 可拔插,没有也不影响
  3. 简化配置,可扩展
  4. 功能可自定制原则
  5. 断网提示
  6. 接口异常自动重试
  7. 利用TS特性,IDE可提示传入参和响应参

举个栗子,这里以 vue3-vite2-ts 作为示范。

封装思路

利用 vue-request 状态管理、请求的节流防抖等天然特性加之到Axios身上

将 Axios 和 vue-request 进行整合出新的语言糖

const { data, run, error, loading } = service.use(axiosConfig).run(Options)

参数 axiosConfig 为原 axios 的 请求配置

参数 Options 为原 vue-request 的 Options 参数

解构出来的变量为响应式数据。详细见 公共 API | VueRequest

部分代码

核心类:

/**
 * axios封装类,通常这个类不需要作必要的修改配置
 * 支持防抖/节流/锁重/断网重连
 * 可拦截重复请求(拦截后面的),并将请求结果共享给所有请求源
 * 支持 get/post 等多种请求方式,支持自定义headers
 */
import axios, { type AxiosInstance, type AxiosError, type AxiosRequestConfig } from 'axios';
import type { AxiosInterceptors, RequestConfig, useRequestOptions } from './type';
import { useRequest, type Service } from 'vue-request';

class Request {
  instance: AxiosInstance;
  interceptors?: AxiosInterceptors;

  constructor(config: RequestConfig) {
    // 请求状态码
    const showStatus = (status: number, msg: string) => {
      let message = '';
      switch (status) {
        case 400:
          message = '请求错误(400)';
          break;
        case 401:
          message = '未授权,请重新登录(401)';
          break;
        case 403:
          message = '拒绝访问(403)';
          break;
        case 404:
          message = '请求出错(404)';
          break;
        case 408:
          message = '请求超时(408)';
          break;
        case 500:
          message = '服务器错误(500)';
          break;
        case 501:
          message = '服务未实现(501)';
          break;
        case 502:
          message = '网络错误(502)';
          break;
        case 503:
          message = '服务不可用(503)';
          break;
        case 504:
          message = '网络超时(504)';
          break;
        case 505:
          message = 'HTTP版本不受支持(505)';
          break;
        default:
          message = msg ?? `连接出错(${status})`;
      }
      return `${message},请稍后重试!`;
    };

    // 初始化axios实例
    this.instance = axios.create(config);
    // 实例拦截器
    this.interceptors = config.interceptors;

    // 注册实例的拦截器
    this.instance.interceptors.request.use(this.interceptors?.requestFulfilled, this.interceptors?.requestRejected);
    this.instance.interceptors.response.use(this.interceptors?.responseFulfilled, this.interceptors?.responseRejected);

    // 注册全局请求拦截器
    this.instance.interceptors.request.use(
      (config: AxiosRequestConfig<any>) => {
        return config;
      },
      (error: AxiosError) => {
        return Promise.reject(error);
      }
    );

    // 注册全局响应拦截器
    this.instance.interceptors.response.use(
      (res: any) => {
        //发生错误返回错误信息
        if (axios.isAxiosError(res)) {
          return Promise.reject(res);
        }
        if ('status' in res && res.status !== 200) {
          const status = res.status || 0;
          res.message = showStatus(status, '');
          return Promise.reject(res);
        }
        //返回成功的响应数据
        return res.data;
      },
      (error: AxiosError) => {
        //取消请求,不报错并返回空值
        if (axios.isCancel(error)) {
          return;
        }
        //处理http错误,抛到业务代码
        if (axios.isAxiosError(error)) {
          const status = error.request.status || 0;
          if (status == 0 || status == 500) {
            return new Promise((resolve, reject) => {
              const img = new Image();
              //临时判断网络是否通畅
              img.src = 'https://www.baidu.com/favicon.ico?_t=' + Date.now();
              img.onload = function () {
                error.message = showStatus(status, error.message);
                reject(error);
              };
              img.onerror = function () {
                error.message = '断网了,请注意您的网络连接';
                reject(error);
              };
            });
          }
          error.message = showStatus(status, error.message);
          return Promise.reject(error);
        }
        return Promise.reject(error);
      }
    );
  }

  request<T>(config: RequestConfig<T>): Promise<T> {
    return new Promise((resolve, reject) => {
      if (config.interceptors?.requestFulfilled) {
        config = config.interceptors.requestFulfilled(config);
      }
      this.instance
        .request<any, T>(config)
        .then(res => {
          if (config.interceptors?.responseFulfilled) {
            res = config.interceptors.responseFulfilled(res);
          }
          resolve(res);
        })
        .catch(err => {
          if (config.interceptors?.responseRejected) {
            err = config.interceptors.responseRejected(err);
          }
          reject(err);
        });
    });
  }

  use<R, P extends unknown[] = any>(config: RequestConfig<R>) {
    const req = (options?: useRequestOptions<R, P>) => {
      const _Service: Service<R, P> = (...args: P) => {
        config.data = args.length > 0 ? args[0] : undefined;
        config.params = args.length > 1 ? args[1] : undefined;
        return this.request<R>({ ...config });
      };
      return useRequest<R, P>(_Service, options);
    };
    return {
      run: (options?: useRequestOptions<R, P>) => {
        return req(options);
      }
    };
  }

  get<T>(url: string, params?: object, config?: RequestConfig<T>): Promise<T> {
    config = Object.assign(config || {}, { params });
    return this.instance.get(url, config);
  }

  post<T>(url: string, data?: object, config?: AxiosRequestConfig): Promise<T> {
    return this.instance.post(url, data, config);
  }
}

export default Request;

实例化:

// service 实例化统一出口
import axios, { type AxiosRequestHeaders } from 'axios';
import { BASE_URL, TIME_OUT } from '@/config';
import AxiosRequest from './request';

// 用于存储请求的标识,便于路由切换时取消请求(仅取消请求不返回响应,并不会截断响应,即前端取消了请求, 实质后端还是会响应的)
const pendingMap = new Map();

/**
 * 生成唯一的每个请求的唯一key
 * @param config
 * @return string
 */
function getPendingKey(config: any) {
  const { url, method } = config;
  return [url, method].join('&');
}

/**
 * 储存每个请求的唯一cancel回调, 以此为标识
 * @param config
 */
function addPending(config: any) {
  const pendingKey = getPendingKey(config);
  config.cancelToken = new axios.CancelToken(cancel => {
    if (!pendingMap.has(pendingKey)) {
      pendingMap.set(pendingKey, cancel);
    }
  });
}

/**
 * 删除重复的请求
 * @param config
 */
function removePending(config: any) {
  const pendingKey = getPendingKey(config);
  if (pendingMap.has(pendingKey)) {
    const cancelToken = pendingMap.get(pendingKey);
    cancelToken(pendingKey);
    pendingMap.delete(pendingKey);
  }
}

const axiosRequest = new AxiosRequest({
  baseURL: BASE_URL || '',
  timeout: TIME_OUT || 0, // 超时时间,单位毫秒
  timeoutErrorMessage: '请求超时',
  withCredentials: true, //跨域携带cookie

  // 配置实例拦截器
  interceptors: {
    requestFulfilled: config => {
      //清除上次请求,防止重复请求
      removePending(config);
      addPending(config);

      // 携带token的拦截
      if (localStorage.getItem('token')) {
        (config.headers as AxiosRequestHeaders).Authorization = ('Bearer ' + localStorage.getItem('token')) as string;
      }

      // 防止GET请求缓存而追加时间戳
      if (config.method?.toUpperCase() === 'GET') {
        config.params = { ...config.params, _t: new Date().getTime() };
      }

      // 防止后端无法获取传统表单POST参数
      if (typeof config.data == 'object' && Object.values(config.headers as AxiosRequestHeaders).includes('application/x-www-form-urlencoded')) {
        //亦可用 import qs from 'qs' 依赖进行处理
        //config.data = qs.stringify(config.data)

        //直接对象格式化处理
        config.data = Object.keys(config.data)
          .map(key => encodeURIComponent(key) + '=' + encodeURIComponent(config.data[key]))
          .join('&');
      }

      return config;
    },
    requestRejected: err => {
      return Promise.reject(err);
    },

    responseFulfilled: res => {
      removePending(res.config);
      return res;
    },
    responseRejected: err => {
      err.config && removePending(err.config);
      return Promise.reject(err);
    }
  }
});

/**
 * 清空所有请求(通常在路由跳转时调用)
 */
export const clearAllPending = () => {
  pendingMap.forEach((cancelToken, pendingKey) => {
    pendingKey && cancelToken(pendingKey);
    pendingMap.delete(pendingKey);
  });
};

export const METHOD = {
  PUT: 'put',
  DELETE: 'delete',
  GET: 'get',
  POST: 'post'
};

/** 接口响应通用格式 */
export interface IDataType<T = any> {
  code: number;
  message?: string;
  data: T;
}

export default axiosRequest;

使用示例:

import service, { METHOD, type IDataType } from '@/service';

const { data, run, error, loading } = service.use({
    url: '/test',
    method: METHOD.POST
}).run({manual: false});

run({username:'admin'});

按照TypeScript的风格封装了axios,需要的朋友可以前往vue3-vite2-ts直接拿来使用,对自己来说也是一次学习的收获。封装axios并不难,重点是请求拦截器和响应拦截器,只是要注意ts的类型约束。