含义解释:
- 取消重复请求: 完全相同的接口在上一个pending状态时,自动取消下一个请求
- 请求失败自动重试: 接口请求后台异常时候,自动重新发起多次请求,直到达到所设次数
- 请求接口数据缓存:接口在设定时间内不会向后台获取数据,而是直接拿本地缓存
本文主要针对使用axios 作为http请求的场景,用到了axios的拦截器。
axios 的拦截器类似于洋葱模型,请求拦截先写的后执行,响应拦截先写的先执行
本来打算封装成多个拦截器来使用的,但是由于各个拦截器之间相当于管道操作,前后影响和干扰,且取消重复请求和请求缓存的部分逻辑存在冲突,故直接写在了一个拦截器中.
调用方式
采用按需传参的方式,即在每个接口中根据该接口的具体情况设置使用哪个功能,设置字段如下:
cancelRequest: true // 接口中定义该项则开启取消重复请求功能
retry: 3, retryDelay: 1000 // retry 请求重试次数,retryDelay 两次重试之间的时间间隔
cache: true, setExpireTime: 30000 // cache: true 开启当前接口缓存,setExpireTime 当前接口缓存时限
使用场景
可以用于axios使用的所有场景,包括Nodejs服务端和Vue等客户端,本文案例目前基于vue前端
案例代码
请求接口调用api.js
import request from './index';
export default {
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 }) // 测试缓存请求参数值不一样
};
axios 实例化
index.js
import { clearToken, getToken } from '@/tools/cookiesStorage.js'; // 导入vuex
import Axios from 'axios'; // 此处引入axios官方文件
import { addPendingRequest, removePendingRequest } from '../tools/encapsulationAxios/cancelRepeatRquest'; // 取消重复请求
import { againRequest } from '../tools/encapsulationAxios/requestAgainSend'; // 请求重发
import { requestInterceptor as cacheReqInterceptor, responseInterceptor as cacheResInterceptor } from '@/tools/encapsulationAxios/requestCache.js';
import { Notification } from 'element-ui';
// 返回结果处理
// 自定义约定接口返回{code: xxx, data: xxx, msg:'err message'}
const responseHandle = {
200: response => {
return response.data.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);
}
};
const axios = Axios.create({
baseURL: process.env.VUE_APP_BASEURL || '',
timeout: 50000
});
// 添加请求拦截器
axios.interceptors.request.use(
function(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);
}
// pendding 中的请求,后续请求不发送(由于存放的peddingMap 的key 和参数有关,所以放在参数处理之后)
addPendingRequest(config); // 把当前请求信息添加到pendingRequest对象中
// 请求缓存
cacheReqInterceptor(config, axios);
return config;
},
function(error) {
return Promise.reject(error);
}
);
// 添加响应拦截器
axios.interceptors.response.use(
response => {
// 响应正常时候就从pendingRequest对象中移除请求
removePendingRequest(response);
cacheResInterceptor(response);
return responseHandle[response.data.code || 'default'](response);
},
error => {
// 从pending 列表中移除请求
removePendingRequest(error.config || {});
// 需要特殊处理请求被取消的情况
if (!Axios.isCancel(error)) {
// 请求重发
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);
}
);
export default axios;
取消重复请求文件封装---cancelRepeatRquest.js:
// 取消重复请求
/* 假如用户重复点击按钮,先后提交了 A 和 B 这两个完全相同(考虑请求路径、方法、参数)的请求,我们可以从以下几种拦截方案中选择其一:
1. 取消 A 请求,只发出 B 请求(会导致A请求已经发出去,被后端处理了)
2. 取消 B 请求,只发出 A 请求
3. 取消 B 请求,只发出 A 请求,把收到的 A 请求的返回结果也作为 B 请求的返回结果
第3种方案需要做监听处理增加了复杂性,结合我们实际的业务需求,最后采用了第2种方案来实现,即:
只发第一个请求。在 A 请求还处于 pending 状态时,后发的所有与 A 重复的请求都取消,实际只发出 A 请求,直到 A 请求结束(成功/失败)才停止对这个请求的拦截。
*/
import Axios from 'axios';
import { generateReqKey } from './commonFuns';
// addPendingRequest :用于把当前请求信息添加到pendingRequest对象 中;
const pendingRequest = new Map(); // Map对象保存键值对。任何值(对象或者原始值) 都可以作为一个键或一个值。
export function addPendingRequest(config) {
if (config.cancelRequest) {
const requestKey = generateReqKey(config);
if (pendingRequest.has(requestKey)) {
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:检查是否存在重复请求,若存在则取消已发的请求。
export function removePendingRequest(response) {
if (response && response.config && response.config.cancelRequest) {
const requestKey = generateReqKey(response.config);
// 判断是否有这个 key
if (pendingRequest.has(requestKey)) {
const cancelToken = pendingRequest.get(requestKey);
cancelToken(requestKey);
pendingRequest.delete(requestKey);
}
}
}
请求重发封装requestAgainSend.js:
// 实现 请求错误时重新发送接口
import { isJsonStr } from './commonFuns';
/**
* @param {失败信息} err
* @param {实例化的单例} axios
* @returns
*/
export function againRequest(err, axios) {
let config = err.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(err);
}
// 重试次数
config.__retryCount += 1;
// 延时处理
var backoff = new Promise(function(resolve) {
setTimeout(function() {
resolve();
}, config.retryDelay || 1000);
});
// 重新发起axios请求
return backoff.then(function() {
// 判断是否是JSON字符串
// TODO: 未确认config.data再重发时变为字符串的原因
if (config.data && isJsonStr(config.data)) {
config.data = JSON.parse(config.data);
}
return axios(config);
});
}
请求缓存requestCache.js
import Axios from 'axios';
import { generateReqKey } from './commonFuns';
const options = {
storage: true, // 是否开启loclastorage缓存
storageKey: 'apiCache',
storage_expire: 600000, // localStorage 数据存储时间10min(刷新页面判断是否清除)
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);
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);
export 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中
}
}
}
export 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();
}
公共函数 commonFuns.js
import Qs from 'qs';
// generateReqKey :用于根据当前请求的信息,生成请求 Key;
export 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字符串
export let isJsonStr = str => {
if (typeof str == 'string') {
try {
var obj = JSON.parse(str);
if (typeof obj == 'object' && obj) {
return true;
} else {
return false;
}
} catch (e) {
console.log('error:' + str + '!!!' + e);
return false;
}
}
};
今天的分享就结束了,也欢迎关注公众号 前端之帆 进行交流和查看更多前端文章,欢迎大家提出宝贵建议!