axios中的请求拦截(防止多次点击按钮多次请求造成的带宽浪费)

1,465 阅读7分钟

axios中的请求拦截(防止多次点击按钮多次请求造成的带宽浪费)

原理:axios请求中有一个cancel token的api

在网上找了许多请求的拦截,但很多都是在请求开始时进行数组存储CancelToken方法与比对信息,返回后进行处理。按照其代码实现了一遍,结果发现双击两次可以实现请求cancel,但多次点击会发生多次请求的情况,故思索了一遍原因。

只有第一次会被释放

以下是未修改前的代码(一些注释是我自己的理解,错误的话还望指出):

// req-intercept.js

let requestList = []; // api请求记录

/**
 * 将当前请求记录到缓存中
 * @param {any} config 请求的内容
 * @param {function} funcCancel 取消请求函数
 */
export const add = (config, funcCancel) => {
    if (!config) {
        return false;
    }

    let req = genReq(config);
    for (let index = 0, len = requestList.length; index < len; index++) {
        if (req === requestList[index].req) {
            requestList[index].funcCancel();
            // requestList.splice(index, 1); // 把这条记录从数组中移除
            // console.log('cancel request:', config.url);
            return;
        }
    }

    requestList.push({
        req: genReq(config),
        funcCancel
    });
};

/**
 * 将请求完成的对象从缓存中移除
 * @param {any} config 请求对象
 */
export const remove = (config) => {
    if (!config) {
        return false;
    }
    let req = genReq(config);
    for (let index = 0, len = requestList.length; index < len; index++) {
        if (req === requestList[index].req) {
            requestList.splice(index, 1); // 把这条记录从数组中移除
            break;
        }
    }
};

// 当前请求的api是否已有记录
export const has = (config) => {
    if (!config) {
        return false;
    }
    let req = genReq(config);
    for (let index = 0, len = requestList.length; index < len; index++) {
        if (req === requestList[index].req) {
            return true;
        }
    }
    return false;
};

/**
 * 生成请求记录对象,方面对比
 * @param {object} config 请求对象
 */
const genReq = (config) => {
    if (!config) {
        return '';
    }
    let arrayReq = [];
    arrayReq.push(`url:${config.url},`);
    arrayReq.push(`method:${config.method},`);
    arrayReq.push(`params:${json2Form(config.params)},`);
    arrayReq.push(`header:${json2Form(config.header)}`);
    // let key = 'Method: ' + config.method + ',Url: ' + url + ',Data: ' + json2Form(data) + ', header:' + json2Form(header);
    return arrayReq.join('');
};

const json2Form = (json) => {
    var str = [];
    for (var key in json) {
        if (key !== 't') {
            str.push(encodeURIComponent(key) + '=' + encodeURIComponent(json[key]));
        }
    }
    return str.join('&');
};
// index.js

import axios from 'axios';
import JSONbig from 'json-bigint';
import * as reqIntercept from './req-intercept.js';
let cancelToken = axios.CancelToken;
let gOpts = {};
const JSONbigString = JSONbig({'storeAsString': true});

let axiosNew = axios.create({
    transformResponse: [function (data) {
        // return JSONbig.parse(data)
        // return JSON.parse(data);

        // 处理数据中的精度溢出的数字,将溢出的数字转换成字符串
        return JSONbigString.parse(data);
    }]
});

function defaultHeader () {
    return {};
};
/**
 * 分析后端返回的错误信息
 * @param responseHeader 后台的相应头对象
 */
const analyzeException = (responseHeader) => {
    if (responseHeader) {
        const errCode = responseHeader['head.err.code'];
        const errMsg = responseHeader['head.err.msg'];

        return {
            errCode,
            // errMsg: errMsg ? decodeURIComponent(errMsg) : SERVICE_ERROR_STATUS[errCode]
            errMsg: errMsg ? decodeURIComponent(errMsg) : errCode
        };
    }
};

// 请求拦截器,每个请求发出之前需要通过此函数处理一遍
axiosNew.interceptors.request.use(function (config) {
    config.cancelToken = new cancelToken((c) => {
        reqIntercept.add(config, c);
    });
    config.baseURL = gOpts.baseURL;
    // 注入自定义请求头
    config.headers = Object.assign({}, config.headers, defaultHeader());
    return config;
}, function (error) {
    return Promise.reject(error);
});

// 响应拦截器,从服务端获取的数据,都统一处理
axiosNew.interceptors.response.use(function (response) {
    reqIntercept.remove(response.config); // 在一个ajax响应后再执行一下取消操作,把已经完成的请求从pending中移除
    if (response.headers) {
        if (response.status === 200) {
            return response.data;
        } else {
            return Promise.reject(analyzeException(response.headers));
        }
    } else {
        // 兼容webpack-dev-server热加载,拦截器中获取的response只有消息体,直接返回消息体的内容
        return response;
    }
}, function (error) {
    if (!error.__CANCEL__) {
        return Promise.reject(analyzeException(error.response));
    }
    // return Promise.reject(analyzeException(error.response.headers));
});

const request = (url, params, method = 'GET', headerData = {}) => {
    method = method.toUpperCase();
    let config = {
        url: url,
        method: method,
        headers: headerData
    };
    if (method === 'POST' || method === 'PUT') {
        Object.assign(config, {
            data: params
        });
    } else {
        Object.assign(config, {
            params: Object.assign({}, params, {
                t: new Date().getTime()
            })
        });
    }
    return new Promise((resolve, reject) => {
        axiosNew(config).then((data) => {
            if (data) {
                resolve(data);
            } else {
                resolve(undefined);
            }
        }).catch(error => {
            reject(new Error(error));
        });
    });
};

export const axiosInterceptors = (target) => {
    target.prototype.responseInterceptors = (response) => {
        console.log('responseInterceptors11');
        return response;
    };

    target.prototype.fetch = (url, params, method, headerData = {}) => {
        return request(url, params, method, headerData);
    };
    /** 设置全局配置参数 */
    target.prototype.setOpts = (opts) => {
        gOpts = opts;
    };
};

随后查找原因,发现在req-intercept.js中的add方法中,如果请求的req判断一致后,会执行cancelToken函数,但随后也会被终止push;

/**
 * 将当前请求记录到缓存中
 * @param {any} config 请求的内容
 * @param {function} funcCancel 取消请求函数
 */
export const add = (config, funcCancel) => {
    if (!config) {
        return false;
    }

    let req = genReq(config);
    for (let index = 0, len = requestList.length; index < len; index++) {
        if (req === requestList[index].req) {
            requestList[index].funcCancel();
            // requestList.splice(index, 1); // 把这条记录从数组中移除
            // console.log('cancel request:', config.url);
            return;
        }
    }

    requestList.push({
        req: genReq(config),
        funcCancel
    });
};

随后去除了return之后:

去除return

这样的请求在我看来是不正确的,非常令人不适。

随后的修改结果:

/**
 * 将当前请求进行去重
 * @param {object} req 当前请求
 * @param {array} requestList api请求记录
 * @param {string} key 比对的信息key值
 * @param {number} num 比对的个数
 */
class repetition {
    constructor ({
        req,
        requestList,
        key,
        num
    }) {
        this.requestList = requestList;
        this.req = req;
        this.requestList.push(req);
        this.key = key;
        this.num = num || 1;
        this.cancelList = [];
    }
    cancelReq () {
        let count = 0;
        for (let i = 0; i < this.requestList.length; i++) {
            console.log(this.req[this.key], this.requestList[i][this.key]);
            if (this.req[this.key] === this.requestList[i][this.key]) {
                if (count > 0) {
                    this.requestList[i].funcCancel();
                    console.log('请求被释放');
                    this.cancelList.push(i);
                }
                count++;
            }
        }

        for (let j = 0; j < this.cancelList.length; j++) {
            this.requestList.splice(this.cancelList[j], 1);
        }
        this.cancelList = [];
        return count;
    }
}

/**
 * 将当前请求记录到缓存中
 * @param {any} config 请求的内容
 * @param {function} funcCancel 取消请求函数
 */
export const add = (config, funcCancel) => {
    if (!config) {
        return false;
    }

    let obj = {
        req: genReq(config),
        funcCancel
    };
    let repetit = new repetition({
        req: obj,
        requestList,
        key: 'req'
    });
    repetit.cancelReq();
    // for (let index = 0, len = requestList.length; index < len; index++) {
    //     if (req === requestList[index].req) {
    //         console.error('-------执行funcCancel---------');
    //         requestList[index].funcCancel();
    //         // requestList.splice(index, 1); // 把这条记录从数组中移除
    //         // console.log('cancel request:', config.url);
    //         return;
    //     }
    // }
};

实验的结果:

相同的请求

现在达到的效果是:相同的接口请求,只有等上个结果请求返回后才能进行下个请求。

以下是全部代码:

// index.js
import axios from 'axios';
import JSONbig from 'json-bigint';
import { Message } from 'element-ui';
import { getGlobalConf } from '@/utils/grocer';
// import { APP_ID, SERVICE_ERROR_STATUS } from '@/common/constants';
import * as reqIntercept from './req-intercept.js';

let gOpts = {};
let cancelToken = axios.CancelToken;
const conf = getGlobalConf();
const JSONbigString = JSONbig({'storeAsString': true});

let axiosNew = axios.create({
    transformResponse: [function (data) {
        // return JSONbig.parse(data)
        // return JSON.parse(data);
        // 处理数据中的精度溢出的数字,将溢出的数字转换成字符串
        return JSONbigString.parse(data);
    }]
});

function defaultHeader () {
    return {
        // 'apiVersion': '1.0'
        // 'zhsession': getZHSessionStore()
    };
};

/**
 * 检查登录状态,如果登录超时,跳转到登录页面
 * @returns 登录超时返回 false
 */
const checkLogin = (resData) => {
    if (resData && typeof resData === 'string' && resData.indexOf('忘记密码1') > -1) {
        window.location.href = '/base/login';
        return false;
    }
    return true;
};

/**
 * 分析后端返回的错误信息
 * @param responseHeader 后台的相应头对象
 */
const analyzeException = (responseData) => {
    if (responseData) {
        return {
            resultCode: responseData.code,
            // errMsg: errMsg ? decodeURIComponent(errMsg) : SERVICE_ERROR_STATUS[errCode]
            resultMsg: responseData.code
        };
    }
};

// 请求拦截器,每个请求发出之前需要通过此函数处理一遍
axiosNew.interceptors.request.use(function (config) {
    if (config.url.indexOf('http://') < 0) {
        config.baseURL = conf.baseURL;
    }
    config.cancelToken = new cancelToken((c) => {
        console.log('已添加');
        reqIntercept.add(config, c);
    });
    console.log(3232);
    config.baseURL = gOpts.baseURL;
    // 注入自定义请求头
    config.headers = Object.assign({}, config.headers, defaultHeader());
    return config;
}, function (error) {
    return Promise.reject(error);
});

// 响应拦截器,从服务端获取的数据,都统一处理
axiosNew.interceptors.response.use(function (response) {
    reqIntercept.remove(response.config); // 在一个ajax响应后再执行一下取消操作,把已经完成的请求从pending中移除
    console.log(response);
    if (response.status === 200) {
        if (!checkLogin(response.data)) {
            return;
        }
        if (response.config.method.toUpperCase() !== 'GET') {
            console.log(response, 'get');
            // 兼容企业库和圈子的数据接口
            if (response.data && (response.data.code === 200 || response.data.code === 0 || response.data.errorCode === 0 || response.data.errcode === 0 || response.data.errCode === 0 || response.data.statusCode === '200')) {
                Message({
                    title: '成功',
                    message: '操作完成',
                    type: 'success'
                });
            } else {
                if (response.data.statusCode) {
                    return response;
                } else {
                    let errMsg = response.data.msg || response.data.errmsg || '';
                    Message({
                        title: '失败',
                        message: errMsg,
                        type: 'error'
                    });
                    throw new Error(errMsg);
                }
            }
        }

        if (response.config.method.toUpperCase() === 'POST' && response.data.statusCode) {
            console.log(response, 'post');
            return response;
        }
        console.log(response, 'no');
        return response.data;
    } else {
        Message({
            title: '失败',
            message: '系统异常,请稍后重试。。',
            type: 'error'
        });
        // return Promise.reject(analyzeException(response));
        throw new Error('系统异常,请稍后重试。。');
        // return response.data;
    }
}, function (error) {
    // 判断是否为登录超时。如果登录超时,跳转到登录页面
    if ('response' in error && error.response === undefined) {
        window.location.href = '/base/login';
        return;
    }
    if ('message' in error) {
        return;
    }
    // console.log('axiosNew.interceptors.response error', JSON.stringify(error, null, 2));
    // return Promise.reject(analyzeException(error.response.headers));
    // return Promise.reject(error);
    Message({
        title: '失败',
        message: '后台接口异常',
        type: 'error'
    });
    console.log(error, 'error');
    throw new Error(error);
});

const req = (url, params, method = 'GET', headerData = {}) => {
    return new Promise((resolve, reject) => {
        let config = {
            url: url,
            method: method,
            headers: headerData
        };

        if (method === 'POST' || method === 'PUT' || method === 'DELETE') {
            Object.assign(config, {
                data: params
            });
        } else {
            Object.assign(config, {
                params: Object.assign({}, params, {
                    t: new Date().getTime()
                })
            });
        };

        const arrLink = [
            'cms/lottery',
            'cms/prize',
            'cms/level'
        ];
        const hasLink = (str) => {
            let res = false;
            str += '';
            arrLink.forEach(item => {
                if (str.indexOf(item) !== -1) {
                    res = true;
                };
            });
            return res;
        };
        axiosNew(config).then((data) => {
            console.log(data, '--------------------------------');
            if (data) {
                let resData;
                if ('errorCode' in data && hasLink(config.url)) {
                    resData = data;
                } else {
                    resData = data.data || data;
                }
                resolve(resData);
            } else {
                return false;
                // resolve(undefined);
            }
        }).catch(error => {
            reject(new Error(error));
        });
    });
};

export default req;

// req-intercept.js
let requestList = []; // api请求记录

/**
 * 将当前请求进行去重
 * @param {object} req 当前请求
 * @param {array} requestList api请求记录
 * @param {string} key 比对的信息key值
 * @param {number} num 比对的个数
 */
class repetition {
    constructor ({
        req,
        requestList,
        key,
        num
    }) {
        this.requestList = requestList;
        this.req = req;
        this.requestList.push(req);
        this.key = key;
        this.num = num || 1;
        this.cancelList = [];
    }
    cancelReq () {
        let count = 0;
        for (let i = 0; i < this.requestList.length; i++) {
            console.log(this.req[this.key], this.requestList[i][this.key]);
            if (this.req[this.key] === this.requestList[i][this.key]) {
                if (count > 0) {
                    this.requestList[i].funcCancel();
                    console.log('请求被释放');
                    this.cancelList.push(i);
                }
                count++;
            }
        }

        for (let j = 0; j < this.cancelList.length; j++) {
            this.requestList.splice(this.cancelList[j], 1);
        }
        this.cancelList = [];
        return count;
    }
}

/**
 * 将当前请求记录到缓存中
 * @param {any} config 请求的内容
 * @param {function} funcCancel 取消请求函数
 */
export const add = (config, funcCancel) => {
    if (!config) {
        return false;
    }

    let obj = {
        req: genReq(config),
        funcCancel
    };
    let repetit = new repetition({
        req: obj,
        requestList,
        key: 'req'
    });
    repetit.cancelReq();
    // for (let index = 0, len = requestList.length; index < len; index++) {
    //     if (req === requestList[index].req) {
    //         console.error('-------执行funcCancel---------');
    //         requestList[index].funcCancel();
    //         // requestList.splice(index, 1); // 把这条记录从数组中移除
    //         // console.log('cancel request:', config.url);
    //         return;
    //     }
    // }
};

/**
 * 将请求完成的对象从缓存中移除
 * @param {any} config 请求对象
 */
export const remove = (config) => {
    if (!config) {
        return false;
    }
    let req = genReq(config);
    for (let index = 0, len = requestList.length; index < len; index++) {
        if (req === requestList[index].req) {
            console.log('-- removed---');
            requestList.splice(index, 1); // 把这条记录从数组中移除
            break;
        }
    }
};

// 当前请求的api是否已有记录
export const has = (config) => {
    if (!config) {
        return false;
    }
    let req = genReq(config);
    for (let index = 0, len = requestList.length; index < len; index++) {
        if (req === requestList[index].req) {
            return true;
        }
    }
    return false;
};

/**
 * 生成请求记录对象,方面对比
 * @param {object} config 请求对象
 */
const genReq = (config) => {
    if (!config) {
        return '';
    }
    let arrayReq = [];
    arrayReq.push(`url:${config.url},`);
    arrayReq.push(`method:${config.method},`);
    arrayReq.push(`params:${json2Form(config.params)},`);
    arrayReq.push(`header:${json2Form(config.header)}`);
    // let key = 'Method: ' + config.method + ',Url: ' + url + ',Data: ' + json2Form(data) + ', header:' + json2Form(header);
    return arrayReq.join('');
};

const json2Form = (json) => {
    var str = [];
    for (var key in json) {
        if (key !== 't') {
            str.push(encodeURIComponent(key) + '=' + encodeURIComponent(json[key]));
        }
    }
    return str.join('&');
};