前言

看了网上很多axios的封装,感觉都不是特别完善。于是我写了个比较完整的封装包括以下功能:

  1. 上传下载文件时的header设置
  2. 错误相应的统一处理
  3. 动态加载api
  4. 数据缓存、清除缓存、缓存级别、最大缓存数
  5. 拦截重复请求、页面跳转时取消正在请求

( 刷新token逻辑,刷新token并没有在下面的代码中实现,因为我不知道后台node要怎么刷新token)暂时就想到这些,如果有其他想法可以留言给我

一、安装插件并引入

npm install axios --save 
npm install vue-router --save
npm install nprogress --save
npm install element-ui --save
npm install crypto-js --save
import axios from 'axios'
import router from './router'
import NProgress from 'nprogress'
import { Message } from 'element-ui'
import HmacMd5 from 'crypto-js/md5'

二、创建一个新的axios

const service = axios.create({
    // 设置基础url,拼接规则是  代理的url + 基础url + 请求时的url
    baseURL: '/api',
    // 设置请求超时时间
    timeout: 20000,
    headers: {
        'content-type': 'application/json', // 设置请求体类型
        /*
        常用的请求格式有:
            'application/json': 发送JSON对象
            'multipart/form-data': 需要文件上传时,就使用该格式
        
        根据后台需求发送其他需要放在header里面数据,例如:
            'Authorization': 'Basic ****', //登录验证
        */
    },
    // 默认情况下,后台状态码不返回200的话,是不会收到返回数据的,可以根据需要设置后台返回状态码的区间
    validateStatus: function(status) {
        return status >= 200 && status <= 500;
    },
    // 请求时是否携带cookie信息
    withCredentials: true,
})

三、HTTPrequest拦截和HTTPresponse拦截

//HTTPrequest拦截
service.interceptors.request.use(config => {

    NProgress.start() // start progress bar

    // 一般我们上传文件都是使用form表单,所以这里判断下data是否是form对象,从而改变请求类型
    if (config.data instanceof FormData) {
        config.headers['content-type'] = 'multipart/form-data';
    }

    let token = localStorage.getItem('token');
    if (token) {
        config.headers['Authorization'] = token; // 让每个请求携带token--['Authorization']为自定义key 请根据实际情况自行修改
    }

    // 如果请求的接口返回一个文件,需要我们将responseType设置为blob
    // if (config.url.indexOf('/file/downLoad') != -1) {
    //     config.responseType = 'blob';
    // }

    return config
}, error => {
    return Promise.reject(error)
});

//HTTPresponse拦截
//客户没有连网、网速慢导致超时,这两种情况不会调用response,需要在请求的.catch中处理
service.interceptors.response.use(res => {
    NProgress.done();
    //如果是401则跳转到登录页面
    if (res.status === 401) router.push({ path: '/login' });
    // 如果请求为非200否者默认统一处理
    if (res.status != 200) {
        return Promise.reject(new Error(res.statusText))
    }
    return res;
}, error => {
    NProgress.done();
    return Promise.reject(error);
})

四、创建缓存对象

// 缓存对象
let cache = {
    // 缓存列表
    list: [],
    // 最大缓存数
    MAXCACHE: 5,
    add(data) {
        // 根据缓存级别计算时间戳
        // 如果没有设置level为0,这里的逻辑相当于每10分钟级别下降一级
        data.timestamp = Number(item.level || 0) * (1000 * 60 * 10) + new Date().getTime();
        // 根据时间戳大小判断缓存插入位置
        let index = this.list.findIndex(item => {
            return item.timestamp ? data.timestamp < item.timestamp : true;
        })
        if (index <= 0) {
            this.list.unshift(data);
        } else {
            this.list.splice(index, 0, data);
        }

        if (this.list.length > this.MAXCACHE) {
            this.list.pop();
        }
    },
    find(key) {
        return this.list.find(item => {
            return key === item.key;
        })
    },
    deleteApiName(apiName) {
        this.list = this.list.filter(item => {
            return item.apiName !== apiName;
        })
    },
    clear() {
        this.list = [];
    }
};

五、创建错误处理方法

一般来说

axios 设置缓存大小 axios清除缓存_前端

Status Codeb不等于200的我们统一处理,

而返回体中code返回的不是200,交给页面中各自处理。 

// 把常见错误消息翻译成中文让用户更容易理解,error.message我没仔细研究过,可能和你的返回内容不一致,修改即可。
// 这里处理了几种常见的错误提示,请求不到服务器、客户没有连网、网速慢导致超时。
const error_toast = (error) => {
    switch (error.message) {
        case 'Internal Server Error':
            Message.error('服务器升级中,请稍后再试');
            break;
        case 'Network Error':
            Message.error('请求失败,请检查您的网络');
            break;
        case (/timeout/.test(error.message) ? error.message :
            false):
            Message.error('请求超时,请稍后重试');
            break;
        default:
            Message.error(error.message || '请求失败');
    }

}

六、监听页面跳转

let now_path = '/';
// 监听页面跳转
router.beforeEach((to, from, next) => {
    now_path = to.fullPath; // 纪录当前页面路径,
    for (let item in request_list) {
        // 页面跳转时是否取消请求,使用场景:有些页面中会请求大量数据,且在其他页面中没有相同的接口时应取消请求、
        // 字典类接口在多个页面中都有使用跳转页面时不应取消请求,这样在下一个页面中就可以直接使用缓存内容;
        if (request_list[item].isSkipCancel) {
            // 取消请求
            request_list[item].cancel('pageTurn');
            // 删除请求中的key
            delete request_list[item];
        }
    }
    next();
})

七、创建请求方法

// 定义一个正在请求的列表,用来防止重复请求
let request_list = {};
// 用来在跳转页面时取消重复请求
const CancelToken = axios.CancelToken;

const request = (params, apiName) => {
    // key可以用来判断请求的url、参数是否完全相同
    let key = params.url + HmacMd5(JSON.stringify(params.params) + JSON.stringify(params.data)).toString();
    console.log(`生成当前请求的key为${key}`);
    if (request_list[key]) {
        console.log(`当前请求已在请求列表中`);
        if (now_path != request_list[key].path) {
            console.log(`请求为不同页面发起,请求成功后返回数据`);
            return new Promise((resolve, reject) => {
                request_list[key].promise_list.push({ resolve, reject });
            })
        } else {
            console.log(`请求为相同页面发起,直接返回错误`);
            return Promise.reject({
                code: 301,
                msg: '请求中,请稍等',
            });
        }
    } else if (cache.find(key) !== undefined) {
        console.log('有缓存,直接返回数据');
        return Promise.resolve(cache.find(key).response_data);
    } else {
        return new Promise((resolve, reject) => {
            console.log('没有缓存,发起请求', params);

            // 将api名称、当前页面路径、页面跳转时是否取消请求,这些都加到request_list对应的key中
            request_list[key] = { apiName, path: now_path, isSkipCancel: params.isSkipCancel | false, };

            params.cancelToken = new CancelToken(function executor(c) {
                // 获取到取消请求的方法
                request_list[key].cancel = c;
            })

            // 把回调方法也放进来
            request_list[key].promise_list = [{ resolve, reject }];

            service(params).then(res => {

                console.log('返回请求', res.data);
                request_list[key].promise_list.map(item => {
                    item.resolve(res.data);
                })


                if (params.method !== 'get') {
                    console.log(`请求不是get请求,删除同一个名称的缓存数据`);
                    // 例如:加了一个用户,这时应该删除用户列表的缓存,下次get请求时加载最新的用户列表
                    cache.deleteApiName(apiName);
                } else if (params.isCache || params.isCache === undefined) {
                    console.log(`请求是get请求,缓存返回数据`);
                    // 添加缓存
                    cache.add({
                        response_data: res.data,
                        key,
                        apiName,
                        level: params.level,
                    })
                }
                console.log(`请求完成,在请求列表中删除对应的key`);
                delete request_list[key];
            }).catch(err => {
                if (err.message !== "pageTurn") {

                    request_list[key].promise_list.map(item => {
                        item.reject(err);
                    })

                    console.log(`请求错误,在请求列表中删除对应的key,并根据错误显示提示框`);
                    delete request_list[key];
                    error_toast(err);
                } else {
                    console.log('请求取消');
                }

            })
        })
    }
}

八、动态加载API

 请求每次使用时都需要先import对应的文件,比较麻烦;

// 引入
import { add } from "@/api/tool/code";
// 在方法中
add()

这样做的好处就是不用在一开始就加载大量的api,不好的地方是每个页面都需要手动引入,尤其是一些比较通用的接口上,要再好多界面中引入。

利用Proxy对象可以实现api的自动加载,当然Proxy还可以用一些公用方法上,访问对象方法时自动加载。

let api = {};
api = new Proxy(api, {
    // apiName是访问api下的对象名称也作为文件名称和文件里所有api的名字,在数据缓存中有相关处理
    get(target, key) {
        if (target[key] === undefined) {//在访问api的属性是判断是否有这个属性没有的话就按照名字和固定的路径去加载文件,然后挂载上去,最后返回
            console.log(`没有属性,根据key'${key}'添加`);
            //加载路径可以根据自己的api放置位置和规则修改
            let obj = require('./api/' + key + '.js').default;
            target[key] = {};
            for (let item in obj) {
                target[key][item] = (data) => {
                    return request(obj[item](data), key);
                }
            }

            // 添加一个清空这个文件中接口缓存的方法,使用场景:页面内的刷新按钮,小程序、公众号的下拉刷新
            target[key].$clear = cache.deleteApiName(key);
        }
        return target[key];
    }
})

// 添加一个清空所有缓存的方法,使用场景: 退出登录
api.$clear = cache.clear();

export default api; //这里导出,最后挂载到全局

九、API封装文件

let user = {
    get: ({ current, size }) => {
        return {
            url: '/user',
            method: 'get',
            isCache: true, // 是否缓存,不写默认为缓存
            level: 0, // 缓存级别
            params: { current, size }
        }
    },
    post: (data) => {
        return {
            url: '/user',
            method: 'post',
            data: {
                name: data.name,
                age: data.age
            }
        }
    },
    put: ({ id, name, age }) => {
        return {
            url: '/user',
            method: 'put',
            data: { id, name, age },
        }
    },
    delete: ({ id }) => {
        return {
            url: '/user',
            method: 'delete',
            params: { id: id },
        }
    }
}

export default user;

十、页面中调用

this.$api.user.get({}).then((res) => {});