美文网首页
axios 取消重复请求,请求失败自动重试,请求接口数据缓存

axios 取消重复请求,请求失败自动重试,请求接口数据缓存

作者: Cherry丶小丸子 | 来源:发表于2024-03-18 11:11 被阅读0次

原文地址:https://juejin.cn/post/6968487137670856711

含义解释

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

本文主要针对使用 axios 作为 http 请求的场景,用到了 axios 的拦截器。
axios 的拦截器类似于洋葱模型,请求拦截先写的后执行,响应拦截先写的先执行
本来打算封装成多个拦截器来使用的,但是由于各个拦截器之间相当于管道操作,前后影响和干扰,且取消重复请求和请求缓存的部分逻辑存在冲突,故直接写在了一个拦截器中

调用方式

采用按需传参的方式,即在每个接口中根据该接口的具体情况设置使用哪个功能,设置字段如下

cancelRequest: true // 接口中定义该项则开启取消重复请求功能
retry: 3 // 请求重试次数
retryDelay: 1000 // retryDelay 两次重试之间的时间间隔
cache: true // true 开启当前接口缓存
setExpireTime: 30000 // setExpireTime 当前接口缓存时限
请求接口调用 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 }) // 测试缓存请求参数值不一样
};
在拦截器中使用
import { clearToken, getToken } from '@/tools/cookiesStorage.js'; // 导入vuex
import Axios from 'axios'; // 此处引入 axios官 方文件
import { addPendingRequest, removePendingRequest } from './cancelRepeatRquest'; // 取消重复请求
import { againRequest } from './requestAgainSend'; // 重新发送请求
import { requestInterceptor as cacheReqInterceptor, responseInterceptor as cacheResInterceptor } from './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(
   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;
    },
    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;
取消重复请求
// 取消重复请求
/* 假如用户重复点击按钮,先后提交了 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 const 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 const 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 const againRequest = (err, axios) => {
    let config = err.config;
    // 如果配置不存在或未设置重试选项,则拒绝
    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(resolve => {
        setTimeout(() => {
            resolve();
        }, config.retryDelay || 1000);
    });
    // 重新发起 axios 请求
    return backOff.then(() => {
        // 判断是否是 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() }));
})();

const getCacheItem = key => {
    let cache = window.localStorage.getItem(options.storageKey);
    let { data, storageExpire } = JSON.parse(cache);
    return (data && data[key]) || null;
}
const 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 const 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 const 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';

// 生成请求的唯一 key
export const generateUniqueKey = config => {
    // 唯一 key = url + 请求方式 + 参数 key 集合 + 参数 value 集合
    // 不直接 JSON.stringify(config.data) 是因为请求前 data 是 { key: value }, 请求后的 data 是 { 'key': 'value' } 格式,所以转成 json 字符串并不相等

    const { url, method, params, data } = config;
    return [url, method, JSON.stringify(Object.keys(data)), JSON.stringify(Object.values(data))].join('&');
    // return `${url}&${method}&${JSON.stringify(Object.keys(data))}&${JSON.stringify(Object.values(data))}`;
}

// generateReqKey:用于根据当前请求的信息,生成请求 Key
export const 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;
        }
    }
};

相关文章

网友评论

      本文标题:axios 取消重复请求,请求失败自动重试,请求接口数据缓存

      本文链接:https://www.haomeiwen.com/subject/dtxlvdtx.html