axios请求的二次封装

144 阅读4分钟

准备内容

  • 引入 axios 插件
  • 引入项目工具类 getToken, checkToken
  • 引入 qs 库
  • 引入组件库中部分组件 Message 和 Loading
  • 引入 不需要loading的url 对象 notShowLoading
  • 引入仓库 store
  • 引入路由对象 router
import axios from 'axios';
import { getToken, checkToken } from '@/util/auth';
import QS from 'qs';
import { Message, Loading } from 'element-ui';
import notShowLoading from './notShowLoaing';
import store from '@/store/index';
import router from '@/router/index';

loading

定义全局变量

// 定义loading变量,
// 以服务的方式(Loading.service(options))调用的 Loading 需要异步关闭
let loading=null,  // loading实例
 needLoadingRequestCount = 0; // 接口请求数量

notShowLoading.js文件

导出不需要加载loading效果的对象,访客标签接口,访客信息接口,访客离开接口

key:不需要loading的请求url地址,value: {isNotShow: true}

// notShowLoading.js 文件
export default {
  '/client/visitor/leaveMsg': { 
    isNoShow: true
  },
  '/client/visitor/info': { 
    isNoShow: true
  },
  '/client/visitor/tag': { 
    isNoShow: true
  },
  // ...
}

开始加载loading函数

function startLoading() {
  //使用Element loading-start 方法
  loading = Loading.service({ // loading实例
    lock: true,
    text: '加载中……',
    background: 'rgba(0, 0, 0, 0.5)',
  });
}

全屏加载Loading

// 开启loading 层
function showFullScreenLoading() {
  // needLoadingRequestCount++;
  // 放在'后面'这里判断条件为 0 ,放在'前面'这里判断逻辑为 1 
  if (needLoadingRequestCount === 0) {
    startLoading();
  }
  needLoadingRequestCount++; 
}

关闭全屏Loading

异步关闭Loading

function endLoading() {
  //使用Element loading-close 方法
  loading.close();
  loading = null; // 清空实例
}

隐藏Loading

function tryHideFullScreenLoading() {
  // if (needLoadingRequestCount <= 0) return;
  if (loading && needLoadingRequestCount > 0) {
    needLoadingRequestCount--;
  }
  if (needLoadingRequestCount === 0) {
    endLoading();
  }
}

取消重复请求文件

// 取消重复请求
// 1.  取消重复请求: 完全相同的接口在上一个pending状态时,自动取消下一个请求
// 判断重复请求的方法:只要是请求地址,请求方式,请求参数一样,
// 那么我们就认为请求是一样的
// Map 对象保存键值对,任何值(对象或者原始值)都可以作为一个键或一个值
const pendingRequest = new Map();

// addPendingRequest: 用于把当前请求信息添加到 pendingRequest 对象中;
function addPendingRequest(config) {
  //cancelRequest: true
  // 接口中定义该项则开启取消重复请求功能
  if (config.cancelRequest) {
    // 该请求唯一标识符
    const requestKey = generateReqKey(config);
    if (pendingRequest.has(requestKey)) {
      // cancelToken取消请求配置项
      // config.cancelToken 配置项默认配置
      config.cancelToken = new Axios.CancelToken(cancel => {
        // cancel 函数的参数会作为 promise 的 error 被捕获
        cancel(`${config.url}请求已取消`)
      })
    } else {
      config.cancelToken = config.cancelToken || new Axios.CancelToken((cancel) => {
      pendingRequest.set(requestKey,cancel)
      })
    }
  }
}

// removePendingRequest 检查是否存在重复请求,若存在则取消已发送的请求
function removePendingRequest(response) {
  if (response && response.config && response.config.cancelRequest) {
    const requestKey = generateReqKey(response.config)
    if (pendingRequest.has(requestKey)) {
      const cancelToken = pendingRequest.get(requestKey);
      cancelToken(requestKey)
      pendingRequest.delete(requestKey)
    }
  }
}

请求重发

// 请求重发
// 实现请求发生错误时,重新发送请求
function againRequest(error,axios) {
  const config = error.config
  // config.retry 具体接口配置的重发次数
  if (!config && !config.retry) return Promise.reject(err)
  // 设置用于记录重试计数的变量 默认为 0
  config._retryCount = config._retryCount || 0;
  // 判断是否超过了重试次数
  if (config._retryCount >= config.retry) return Promise.reject(error)
  // 重试次数自增
  config._retryCount += 1;
  //延时处理
  const backoff = new Promise((resolve, reject) => {
    setTimeout(()=>resolve(),config.retryDelay || 1000)
  })
  // 重新发起请求
  return backoff.then(() => {
    // 判断是否是字符串,
    // 未确认config.data 再重发时变为字符串的原因
    if (config.data && isJsonStr(config.data)) {
      config.data = JSON.parse(config.data)
    }
    return axios(config)
  })
}

请求缓存

// 请求缓存
const options = {
  storage: true, // 是否开启localstorage 缓存
  storageKey: 'apiCache',
  storage_expire: 6000, // localStorage 数据存储时间 10 min (刷新页面判断是否清除)
  expire: 20000 //每个接口数据缓存 (ms) 毫秒数
}
  // 初始化 自执行函数
  (function () {
    let cache = window.localStorage.getItem(options.storageKey);
    if (cache) {
      let { storageExpire } = JSON.parse(cache)
      // 未超时不做处理
      if (storageExpire && getNowTime() - storageExpire < options.storage_expire) return;
    }
    window.localStorage.setItem(options.storageKey, JSON.stringify({ data: {}, storageExpire: getNowTime() }))
  })();

  function getCacheItem(key) {
    let cache = window.localStorage.getItem(options.storageKey);
    let { data, storageExpire } = JSON.parse(cache)
    // (data && data[key]) 这句话就是 data[key] 对应的值
    return (data && data[key]) || null;
  }
  function setCacheItem (key,value) {
    let cache = window.localStorage.getItem(options.storageKey);
    let { data, storageExpire } = JSON.parse(cache)
    // 传递过来的值替换原来的值
    data[key] = value;
    // 存储起来
    window.localStorage.setItem(options.storageKey, JSON.stringify({data,storageExpire}))
  }
let _CACHES = {};
// 使用proxy 代理
let cacheHandler = {
  get: function (target,key) {
    let value = target[key];
    console.log(`${key}被读取`, value);
    if (options.storage && !value) {
      value = getCacheItem(key)
    }
    return value;
  },
  set: function (target,key,value) {
    console.log(`${key},被设置为 ${value}`);
    target[key] = value;
    if (options.storage) {
      setCacheItem(key,value)
    }
    return true
  }
}
let CACHES = new Proxy(_CACHES,cacheHandler)

function requestInterceptor(config,axios) {
  // 开启缓存则保存请求结果和cancel 函数
  if (config.cache) {
    // 获取数据
    let data = CACHES[`${generateReqKey(config)}`];
    // 这里用于存储是默认时间还是用户传递过来的时间
    let setExpireTime;
    config.setExpireTime ? (setExpireTime = config.setExpireTime) : (setExpireTime = options.expire);
    // 判断缓存数据是否存在 存在的话 是否过期 没过期就返回
    if (data && ((getNowTime() - data.expire) < setExpireTime)) {
      config.cancelToken = new Axios.CancelToken((cancel) => {
        // cancel 函数的参数会作为 promise 的error 被捕获
        cancel(data)
      }) // 传递结果到catch 中
    }
    // else {
    //   // 不存在的话或者过期,设置
    // }
  }
}

function responseInterceptor(response) {
  // 返回的code === 200 时候才会缓存下来
  if (response && response.config.cache && response.data.code === 200) {
    let data = {
      expire: getNowTime(),
      data: response
    }
    CACHES[`${generateReqKey(response.config)}`] = data
  }
}

// 获取当前时间戳
function getNowTime() {
  return new Date().getTime(); // 毫秒数
}

公共方法

// https://juejin.cn/post/6968487137670856711#heading-0
// 取消重复请求: 完全相同的接口在上一个pending状态时,自动取消下一个请求
// 请求失败自动重试: 接口请求后台异常时候,自动重新发起多次请求,直到达到所设次数
// 请求接口数据缓存:接口在设定时间内不会向后台获取数据,而是直接拿本地缓存

const testAPI= {
  middleViewData: data => request.get('/jscApi/middleViewData', { data }), // 正常请求
  cancelReq: data => request.get('http://localhost:3003/jscApi/middleViewData', { data, cancelRequest: true }), // 测试取消请求
  reqAgainSend: data => request.get('/equ/equTypeList11', { data, retry: 3, retryDelay: 1000 }), // 测试请求重发,除了原请求外还会重发3次
  cacheEquList: data => request.get('/equ/equList', { data, cache: true, setExpireTime: 30000 }), // 测试缓存请求带参数:setExpireTime 为缓存有效时间ms
  cacheEquListParams: data => request.get('/equ/equList', { data, cache: true }) // 测试缓存请求参数值不一样
};
// cancelRequest: true,       // 接口中定义该项则开启取消重复请求功能
// retry: 3, retryDelay: 1000,  // retry 请求重试次数,retryDelay 两次重试之间的时间间隔
// cache: true, setExpireTime: 30000,  // cache: true 开启当前接口缓存,setExpireTime 当前接口缓存时限
function generateReqKey(config) {
  // 响应的时候, response.config 中的data 是一个 JSON字符串,所以需要转换一下
  if (config && config.data && isJsonStr(config.data)) {
    config.data = JSON.parse(config.data)
  }
  const { method, url, params, data } = config // 请求方法,地址,参数,
  return [method, url, qs.stringify(params), qs.stringify(data)].join('&'); // 拼接
}

// 判断一个字符串是否为JSON 字符串
const isJsonStr = str => {
  if (typeof str === 'string') {
     try {
       var obj = JSON.parse(str)
       if (typeof obj === 'object' && obj) {
         return true
       } else {
         return false
       }
     } catch (error) {
       console.log('error:' + str + '!!!' + error);
        return false
     }
   }
}

创建axios实例

const axios = Axios.create({
  baseURL: process.env.VUE_APP_BASEURL || '',
  timeout: 30000
})

const responseHandle = {
  200: response => {
    return response.data
  },
  401: response => {
    Notification({
      title: '认证异常',
      message: '登录状态已过期,请重新登录!',
      type: 'error'
    });
    clearToken();
    window.location.href = window.location.origin;
  },
  default: response => {
    Notification({
      title: '操作失败',
      message: response.data.msg,
      type: 'error'
    });
    return Promise.reject(response)
  }
}
// 请求拦截
axios.interceptors.request.use(config => {
  // 请求头用于接口token 认证
  getToken() && (config.headers['Authorization'] == getToken())

  if (config.method.toLocaleLowerCase() === 'post' || config.method.toLocaleLowerCase() === 'put') {
    // 参数统一处理, 请求都使用data 传参
    config.data = config.data.data
  } else if (config.method.toLocaleLowerCase() === 'get' || config.method.toLocaleLowerCase() === 'delete') {
    // 参数统一处理
    config.params = config.data
  } else {
    alert( `不允许的请求方法: ${config.method}`)
  }
  // pending中的请求,后续请求不发送(由于存放的penddingMap 的 key 和参数有关,所以放在参数处理之后)
  addPendingRequest(config) // 把当前请求信息添加到penddingRequest对象中
  // 请求缓存
  requestInterceptor(config, axios)
  return config;
}, error => { // 有缓存时被取消请求回到error中
  return Promise.reject(error)
})

// 添加响应拦截器
axios.interceptors.response.use(response => {
  // 响应正常的时候就从penddingQuest对象中移除请求
  removePendingRequest(response);
  responseInterceptor(response);
  return responseHandle[response.data.code || 'default'](response)
},
error => {
  // 从 pendingRequest列表中移除请求
  removePendingRequest(error.config || {});
  // 需要特殊处理请求被取消的情况
  if (!axios.isCancel(error)) {// axios.isCancel(thrown) 取消的失败
    // 不是取消引起的失败失败
    // 请求重发
    return againRequest(error,axios)
  }
  // 请求缓存处理方式
  if (axios.isCancel(error) && error.message.data && error.message.data.config.cache) {
    return Promise.resolve(error.message.data.data.data); // 返回结果数据
  }
  return Promise.reject(error)
}
)

创建axios对象 service

创建service对象,并设置跨域是否需要使用凭证

let ishttps = 'https:' == document.location.protocol ? true : false,
  service = null;
if (ishttps) {
  console.log('https');
  service = axios.create({
    timeout: 1200000, // 请求超时时间
    baseURL: process.env.VUE_APP_BASE_API_HTTPS,
  });
} else {
  console.log('http');
  service = axios.create({
    timeout: 1200000, // 请求超时时间
    baseURL: process.env.VUE_APP_BASE_API_HTTP,
  });
}
// 跨域请求是否需要使用凭证
// axios 默认发送请求是不携带cookie的
service.defaults.withCredentials = true; 

请求拦截

//请求拦截器
service.interceptors.request.use(
  (config) => {
    let token;
    if (config.url.indexOf('/auth/oauth/token') >= 0) {
      token = 'basic b25lY2M6cTlzZUhFTHA=';
    } else {
      checkToken();
      token = getToken();
      if (!token) {
         //
        store.dispatch('user/clearInfo');
        // 回到登录页
        router.push('/login');
        return Promise.reject({
          code: -1,
        });
      }
    }
    // console.log(token)
    config.headers['Authorization'] = token;
    const isShow = notShowLoading[config.url];
    if (!isShow) {
      showFullScreenLoading();
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

响应拦截

//响应拦截器
service.interceptors.response.use(
  (res) => {
    tryHideFullScreenLoading();
    // 请求结果包了一层data,可以同意处理
    const data = res.data || res;
    return Promise.resolve(data);
  },
  (error) => {
    tryHideFullScreenLoading();
    if (error && error.response) {
      const { response } = error;
      switch (response.status) {
        // token 过期?
        case 401:
          store.dispatch('user/logout').then(() => {
            router.push('/login');
          });
          break;
        default:
          Message({
            type: 'error',
            message: response.data.msg,
            showClose: true,
          });
          return Promise.reject(response);
      }
    }
  }
);

get/ post / put / delete 请求封装

/** 
  @param url : 地址
  params:参数 不传为默认值
  contentType:请 contentType类型 不传为默认值
  cb: validateStatus() onDownloadProgress()...请求函数等其他额外配置 以对象形式传入
*/
export const $get = (url, params = {}, contentType = 'application/x-www-form-urlencoded', headercb = {}, cb = {}) => {
  return service.get(
    url,
    {
      params: params,
    },
    {
      headers: {
        'Content-Type': contentType,
        ...headercb,
      },
      ...cb,
    }
  );
};
//post
export const $post = (url, params = {}, contentType = 'application/x-www-form-urlencoded', headercb = {}, cb = {}) => {
  let str;
  if (contentType === 'application/x-www-form-urlencoded') {
    str = QS.stringify(params);
  } else {
    str = params;
  }
  return service.post(url, str, {
    headers: {
      'Content-Type': contentType,
      ...headercb,
    },
    ...cb,
  });
};
//put
export const $put = (url, params = {}, contentType = 'application/x-www-form-urlencoded', headercb = {}, cb = {}) => {
  let str;
  if (contentType === 'application/x-www-form-urlencoded') {
    str = QS.stringify(params);
  } else {
    str = params;
  }
  return service.put(url, str, {
    headers: {
      'Content-Type': contentType,
      ...headercb,
    },
    ...cb,
  });
};
//delete
export const $delete = (
  url,
  params = {},
  contentType = 'application/x-www-form-urlencoded',
  headercb = {},
  cb = {}
) => {
  return service.delete(
    url,
    {
      params: params,
    },
    {
      headers: {
        'Content-Type': contentType,
        ...headercb,
      },
      ...cb,
    }
  );
};

export const $download = (url, params = {}, contentType = 'application/x-www-form-urlencoded') => {
  let str;
  if (contentType === 'application/x-www-form-urlencoded') {
    str = QS.stringify(params);
  } else {
    str = params;
  }
  return service.post(url, str, {
    headers: {
      'Content-Type': contentType,
    },
    responseType: 'blob',
  });
};

export const $downloadGet = (url, params = {}, contentType = 'application/x-www-form-urlencoded') => {
  return service.get(url, {
    params: params,
    headers: {
      'Content-Type': contentType,
    },
    responseType: 'blob',
  });
};

http.js文件

整个封装请求完整代码

// http.js文件 

import axios from 'axios';
import { getToken, checkToken } from '@/util/auth';
import QS from 'qs';
import { Message, Loading } from 'element-ui';
import notShowLoading from './notShowLoaing';
import store from '@/store/index';
import router from '@/router/index';

let loading; //定义loading变量
let needLoadingRequestCount = 0;

function startLoading() {
  //使用Element loading-start 方法
  loading = Loading.service({
    lock: true,
    text: '加载中……',
    background: 'rgba(0, 0, 0, 0.5)',
  });
}

function endLoading() {
  //使用Element loading-close 方法
  loading.close();
}

function showFullScreenLoading() {
  if (needLoadingRequestCount === 0) {
    startLoading();
  }
  needLoadingRequestCount++;
}

function tryHideFullScreenLoading() {
  if (needLoadingRequestCount <= 0) return;
  needLoadingRequestCount--;
  if (needLoadingRequestCount === 0) {
    endLoading();
  }
}

let ishttps = 'https:' == document.location.protocol ? true : false,
  service = null;
if (ishttps) {
  console.log('https');
  service = axios.create({
    timeout: 1200000, // 请求超时时间
    baseURL: process.env.VUE_APP_BASE_API_HTTPS,
  });
} else {
  console.log('http');
  service = axios.create({
    timeout: 1200000, // 请求超时时间
    baseURL: process.env.VUE_APP_BASE_API_HTTP,
  });
}
// const service = axios.create({
//   timeout: 5000, // 请求超时时间
//   baseURL: process.env.VUE_APP_BASE_API,
// })

// 跨域请求是否需要使用凭证
// axios 默认发送请求是不携带cookie的
service.defaults.withCredentials = true; 
//请求拦截器
service.interceptors.request.use(
  (config) => {
    let token;
    if (config.url.indexOf('/auth/oauth/token') >= 0) {
      token = 'basic b25lY2M6cTlzZUhFTHA=';
    } else {
      checkToken();
      token = getToken();
      if (!token) {
        store.dispatch('user/clearInfo');
        router.push('/login');
        return Promise.reject({
          code: -1,
        });
      }
    }
    // console.log(token)
    config.headers['Authorization'] = token;
    const isShow = notShowLoading[config.url];
    if (!isShow) {
      showFullScreenLoading();
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

//响应拦截器
service.interceptors.response.use(
  (res) => {
    tryHideFullScreenLoading();
    const data = res.data || res;
    return Promise.resolve(data);
  },
  (error) => {
    tryHideFullScreenLoading();
    if (error && error.response) {
      const { response } = error;
      switch (response.status) {
        case 401: // token 过期?
          store.dispatch('user/logout').then(() => {
            router.push('/login');
          });
          break;
        default:
          Message({
            type: 'error',
            message: response.data.msg,
            showClose: true,
          });
          return Promise.reject(response);
      }
    }
  }
);
/*
   * @param url : 地址
            params:参数 不传为默认值
            contentType:请 contentType类型 不传为默认值
            cb: validateStatus() onDownloadProgress()...请求函数等其他额外配置 以对象形式传入
*/
//get
export const $get = (url, params = {}, contentType = 'application/x-www-form-urlencoded', headercb = {}, cb = {}) => {
  return service.get(
    url,
    {
      params: params,
    },
    {
      headers: {
        'Content-Type': contentType,
        ...headercb,
      },
      ...cb,
    }
  );
};
//post
export const $post = (url, params = {}, contentType = 'application/x-www-form-urlencoded', headercb = {}, cb = {}) => {
  let str;
  if (contentType === 'application/x-www-form-urlencoded') {
    str = QS.stringify(params);
  } else {
    str = params;
  }
  return service.post(url, str, {
    headers: {
      'Content-Type': contentType,
      ...headercb,
    },
    ...cb,
  });
};
//put
export const $put = (url, params = {}, contentType = 'application/x-www-form-urlencoded', headercb = {}, cb = {}) => {
  let str;
  if (contentType === 'application/x-www-form-urlencoded') {
    str = QS.stringify(params);
  } else {
    str = params;
  }
  return service.put(url, str, {
    headers: {
      'Content-Type': contentType,
      ...headercb,
    },
    ...cb,
  });
};
//delete
export const $delete = (
  url,
  params = {},
  contentType = 'application/x-www-form-urlencoded',
  headercb = {},
  cb = {}
) => {
  return service.delete(
    url,
    {
      params: params,
    },
    {
      headers: {
        'Content-Type': contentType,
        ...headercb,
      },
      ...cb,
    }
  );
};

export const $download = (url, params = {}, contentType = 'application/x-www-form-urlencoded') => {
  let str;
  if (contentType === 'application/x-www-form-urlencoded') {
    str = QS.stringify(params);
  } else {
    str = params;
  }
  return service.post(url, str, {
    headers: {
      'Content-Type': contentType,
    },
    responseType: 'blob',
  });
};

export const $downloadGet = (url, params = {}, contentType = 'application/x-www-form-urlencoded') => {
  return service.get(url, {
    params: params,
    headers: {
      'Content-Type': contentType,
    },
    responseType: 'blob',
  });
};