vue判断请求时间超过5秒显示关闭_正确使用Axios,提升接口请求幸福感

1 前景介绍

根据Axios中文文档:Axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。因其实在是过于简单粗暴,好用的一逼,可以算是目前最流行的前后端数据交互的工具,前端人员基本上每天都在接触它。为达到体感更佳的使用效果,一般不会直接原生使用(当然你硬要原生使用也可以),一般需要做几个简单的二次封装动作。本文旨在通过在vue项目中对axios的二次封装使用做一个笔记,分享一下aiox封装中需要关注的几个点。

项目代码文件夹大概如下:

af3fb0f1b5d8b86d1db4b15103a3e25c.png

需要本文前需要说明的是:

  • http文件夹下的index.js文件是对axios封装的文件(本文重点关注讨论这个文件),api文件夹下的文件是使用http接口定义的文件,userStorage.js是跟token和用户信息缓存相关的工具文件。
  • 本项目用的是vantUI框架,所以,跟UI相关的(比如Toast),都是导入vant中的,实际可根据具体使用UI框架替换就好(比如用的antd)。
  • 虽然是vue项目中封装,但是react项目同理。

2 创建axios实例

创建一个axios实例非常简单,调用create()方法就好了:

const http = axios.create({
    //withCredentials: true, //表示跨域请求时是否需要使用凭证,可根据情况自己设定,我这儿用不上
    timeout: 30000, //设置请求超时时间,单位:ms
    //baseURL:'' //http请求域名,也可以在请求拦截器里面设置
});

3 请求拦截器封装

在我看来,请求拦截器的作用主要有两个:

1、对HTTP请求的Request Headers参数进行全局设置

2、设置接口请求的域名和接口路径,也就是baseURL和url

其实现代码片段如下:

http.interceptors.request.use(
    (config) => {
        // config中包含url就是后端给的接口路径
        let { url } = config;
        // 此处我的开发环境中因为使用了webpack的devServer代理,所以接口域名为空
        let baseURL = '';
        // 生产环境(build之后)的域名是直接从浏览器地址栏截取的
        // 所以做出以下判断,可根据实际情况自行判断不同环境的域名
        const isProd = process.env.NODE_ENV === 'production';
        if (isProd) {
            baseURL = `${document.location.protocol}//${document.location.host}/app`;
        }
        config = {
            ...config,
            baseURL: baseURL,
            url: url,
            headers: {
                Accept: 'application/json',
                'Content-Type': 'application/json; charset=utf-8',
                ...config.headers,
                accessToken: UserStorage.getUaaTokenInfo().accessToken || '', //从缓存中取出登录获取的token
                authType: 'account',
            },
        }; 
        return config;
    },
    (error) => {
        return Promise.reject(error);
    }
);

最终在network中看到的Request Headers效果如下:

279a32d8312068a5ac9cb42895b82f82.png

4 响应拦截器封装

响应拦截器的作用主要是以下两点

1、统一处理接口请求返回数据的后,再返回具体给业务层使用

2、统一处理接口异常状态码情况,统一显示错误信息

必须说明的是,这块处理的逻辑其实比较灵活,具体要看接口返回的数据是什么格式,根据具体情况来处理,达到以上目的。因为我们接口返回的restful风格,返回的接口格式如下图:

2379bf6afd54d9a5245d3a33a430b7b9.png

所以,如果你的接口返回跟我的这个类似,那么可以参考如下代码片段:

http.interceptors.response.use(
    (res) => {
        // console.log(res);
        if (res.status === 200) {
            if (res.data) {
                const resStatus = res.data.status;
                const msg = res.data.message || '未知错误';
                switch (resStatus) {
                    case 200:
                        // 请求成功,返回正确的数据结果,其他情况都是返回错误信息
                        return res.data.data;
                    case 401: //登录过期,目前直接跳出系统
                        // 清空了缓存token等
                        UserStorage.clearLogin();
                        Toast(res.data.message || '登录过期,请重新登录');
                        // 清空了缓存之后,刷新直接跳到登录页(需要在路由的地方专门处理)
                        window.location.reload();
                        return;
                    case 500:
                        break;
                }
                Toast(msg);
                return Promise.reject(msg);
            }
            return Promise.reject('未知错误');
        }
        return Promise.reject(res);
    },
    (error) => {
        const status =
            (error.response &&
                error.response.status &&
                error.response.status) ||
            '';
        const data = (error.response && error.response.data) || {};
        if (data.message) {
            Toast(data.message);
            return Promise.reject(data.message);
        }

        if (
            error.code == 'ECONNABORTED' &&
            error.message.indexOf('timeout') != -1
        ) {
            Toast('请求超时~~');
            return Promise.reject('请求超时~~');
        }
        if (status === 401) {
            Toast('登录过期,请重新登录');
            return Promise.reject('登录过期,请重新登录');
        }
        if (status === 404) {
            Toast('接口404报错');
            return Promise.reject('接口404报错');
        }
        if (status === 500) {
            console.log(error.response);
            Toast('服务器错误');
            return Promise.reject('接口404报错');
        }
        return Promise.reject('未知错误');
    }
);
  • 代码中的Toast组件是引用的vant组件,实际根据项目使用的UI框架替换即可。
  • 只有请求成功(status === 200),返回正常的数据,请求失败或者异常,返回相关报错信息

5 全局loading效果封装

经过以上几步,一个二次封装的axios模块其实已经可以使用,这步操作的目的是由于本人比较懒,不想每次请求接口时加loading效果,所以在axios封装模块中实现全局统一封装了loading效果,即实现请求发出,页面就会自动加上loading效果。大概如下图所示:

851900543b0caf5d44133220626b4b24.png

这块的实现参考了这篇文章,在此感谢原作者。

  • 首先在文件中加上显示loading和隐藏loading的方法,以下代码片段:
//loading对象
let loading = null;
//当前正在请求的数量
let needLoadingRequestCount = 0;
//显示loading
function showLoading(target) {
    // 后面这个判断很重要,因为关闭时加了抖动,此时loading对象可能还存在,
    // 但needLoadingRequestCount已经变成0.避免这种情况下会重新创建个loading
    if (needLoadingRequestCount === 0 && !loading) {
        loading = Toast.loading({
            getContainer: target || 'body',
        });
    }
    needLoadingRequestCount++;
}
//隐藏loading
function hideLoading() {
    needLoadingRequestCount--;
    needLoadingRequestCount = Math.max(needLoadingRequestCount, 0); //做个保护
    if (needLoadingRequestCount === 0) {
        //关闭loading
        toHideLoading();
    }
}
//防抖:将 300ms 间隔内的关闭 loading 合并为一次。防止连续请求时, loading闪烁的问题。
let toHideLoading = _.debounce(() => {
    loading && loading.clear();
    loading = null;
}, 300);
  • 改造请求拦截器,接口请求前显示loading,异常情况时隐藏loading,加入代码片段如下:
http.interceptors.request.use(
    (config) => {
        //其他代码....
        
        //判断当前请求是否设置了不显示Loading
        if (config.headers.showLoading !== false) {
            showLoading(config.headers.loadingTarget);
        }
        return config;
    },
    (error) => {
        //判断当前请求是否设置了不显示Loading
        if (error.config.headers.showLoading !== false) {
            hideLoading();
        }
        return Promise.reject(error);
    }
);
  • 改造响应拦截器,接口请求完成后或异常的时候隐藏loading,加入代码片段如下:
http.interceptors.response.use(
    (res) => {
        //判断当前请求是否设置了不显示Loading(不显示自然无需隐藏)
        if (res.config.headers.showLoading !== false) {
            hideLoading();
        }
        // 其他代码...
    },
    (error) => {
        //判断当前请求是否设置了不显示Loading(不显示自然无需隐藏)
        if (error.config.headers.showLoading !== false) {
            hideLoading();
        }
       // 其他代码...
    }
);

以上代码都比较简单,阅读即懂,无需赘述。

6 使用说明

经过以上步骤,封装已经完成,一般是在api目录下的文件中导入使用即可。

使用过程中几个关键点:

  • 引入封装好的axios:
import http from '../http';
  • 发送get请求:
// 演示发送get请求的例子
export function getRequestApi() {
    return http.get(`/api/xxx`);
}
  • 发送post请求:
// 演示发送post请求的例子
export function postRequestApi() {
    return http.get(`/api/xxx`);
}
  • 不想某个接口请求过程中显示loading:(结合第5点来看)
export function hideLoadingApi() {
    return http.get(`/api/xxx`,{headers: {'showLoading': false}});
}
  • 从某个页面发出请求的例子:
   async getCategoryList() {
            try {
                // 注意要导入getRequestApi方法
                const res = await getRequestApi();
                if (res) {
                    //请求成功...
                }
            } catch (error) {
                // 请求异常,这儿可能收到的是相应拦截器中返回的错误信息
                console.log(error);
            }
        },

7 总结

  • axios二次封装主要针对请求和想要拦截器封装
  • 优良的项目代码组织以便获得更好的维护体感
  • 本文提供的是一种满足常用的封装思路,复杂情况可能需要考虑:取消请求,不要重复提交同一个请求,断网提示等

以下源代码直接放出:

  • http-->index.js
import axios from 'axios';
import _ from 'lodash';
import { Toast } from 'vant';
import UserStorage from '../utils/UserStorage';
const http = axios.create({
    //withCredentials: true, //表示跨域请求时是否需要使用凭证,可根据情况自己设定,我这儿用不上
    timeout: 30000, //设置请求超时时间,单位:ms
    //baseURL:''//http请域名,也可以在请求拦截器里面搞
});

//loading对象
let loading = null;
//当前正在请求的数量
let needLoadingRequestCount = 0;
//显示loading
function showLoading(target) {
    // 后面这个判断很重要,因为关闭时加了抖动,此时loading对象可能还存在,
    // 但needLoadingRequestCount已经变成0.避免这种情况下会重新创建个loading
    if (needLoadingRequestCount === 0 && !loading) {
        loading = Toast.loading({
            getContainer: target || 'body',
        });
    }
    needLoadingRequestCount++;
}
//隐藏loading
function hideLoading() {
    needLoadingRequestCount--;
    needLoadingRequestCount = Math.max(needLoadingRequestCount, 0); //做个保护
    if (needLoadingRequestCount === 0) {
        //关闭loading
        toHideLoading();
    }
}
//防抖:将 300ms 间隔内的关闭 loading 合并为一次。防止连续请求时, loading闪烁的问题。
let toHideLoading = _.debounce(() => {
    loading && loading.clear();
    loading = null;
}, 300);

http.interceptors.request.use(
    (config) => {
        // config中包含url就是后端给的接口路径
        let { url } = config;
        // 此处我的开发环境中因为使用了webpack的devServer代理,所以接口域名为空
        let baseURL = '';
        // 生产环境(build之后)的域名是直接从浏览器地址栏截取的
        // 所以做出以下判断,可根据实际情况自行判断不同环境的域名
        const isProd = process.env.NODE_ENV === 'production';
        if (isProd) {
            baseURL = `${document.location.protocol}//${document.location.host}/integrated-app`;
        }
        config = {
            ...config,
            baseURL: baseURL,
            url: url,
            headers: {
                Accept: 'application/json',
                'Content-Type': 'application/json; charset=utf-8',
                ...config.headers,
                accessToken: UserStorage.getUaaTokenInfo().accessToken || '',
                authType: 'account',
            },
        };
        // 判断当前请求是否设置了不显示Loading
        if (config.headers.showLoading !== false) {
            showLoading(config.headers.loadingTarget);
        }
        return config;
    },
    (error) => {
        // 判断当前请求是否设置了不显示Loading
        if (error.config.headers.showLoading !== false) {
            hideLoading();
        }
        return Promise.reject(error);
    }
);

http.interceptors.response.use(
    (res) => {
        //判断当前请求是否设置了不显示Loading(不显示自然无需隐藏)
        if (res.config.headers.showLoading !== false) {
            hideLoading();
        }
        // console.log(res);
        if (res.status === 200) {
            if (res.data) {
                const resStatus = res.data.status;
                const msg = res.data.message || '未知错误';
                switch (resStatus) {
                    case 200:
                        // 请求成功,返回正确的数据结果,其他情况都是返回错误信息
                        return res.data.data;
                    case 401: //登录过期,目前直接跳出系统
                        // 清空了缓存token等
                        UserStorage.clearLogin();
                        Toast('登录过期,请重新登录');
                        // 清空了缓存之后,刷新直接跳到登录页(需要在路由的地方专门处理)
                        window.location.reload();
                        return;
                    case 500:
                        break;
                }
                Toast(msg);
                return Promise.reject(msg);
            }
            return Promise.reject('未知错误');
        }
        return Promise.reject(res);
    },
    (error) => {
        //判断当前请求是否设置了不显示Loading(不显示自然无需隐藏)
        if (error.config.headers.showLoading !== false) {
            hideLoading();
        }
        const status =
            (error.response &&
                error.response.status &&
                error.response.status) ||
            '';
        const data = (error.response && error.response.data) || {};
        if (data.message) {
            Toast(data.message);
            return Promise.reject(data.message);
        }

        if (
            error.code == 'ECONNABORTED' &&
            error.message.indexOf('timeout') != -1
        ) {
            Toast('请求超时~~');
            return Promise.reject('请求超时~~');
        }
        if (status === 401) {
            Toast('登录过期,请重新登录');
            return Promise.reject('登录过期,请重新登录');
        }
        if (status === 404) {
            Toast('接口404报错');
            return Promise.reject('接口404报错');
        }
        if (status === 500) {
            console.log(error.response);
            Toast('服务器错误');
            return Promise.reject('接口404报错');
        }
        return Promise.reject('未知错误');
    }
);

export default http;

  • utils-->userStorage.js
class UserStorage {
    //缓存登陆进入h5之后获取到的用户信息
    static saveUserInfo(userInfo) {
        sessionStorage.setItem('userInfo', JSON.stringify(userInfo));
    }

    //获取缓存中的用户信息
    static getUserInfo() {
        const resUser = sessionStorage.getItem('userInfo');
        if (resUser) {
            return JSON.parse(resUser);
        }
        return {};
    }

    //缓存获取的用户token信息
    static saveUaaTokenInfo(tokenInfo) {
        sessionStorage.setItem('uaaToken', JSON.stringify(tokenInfo));
    }

    //获取缓存中的用户token
    static getUaaTokenInfo() {
        const tokenInfo = sessionStorage.getItem('uaaToken');
        if (tokenInfo) {
            return JSON.parse(tokenInfo);
        }
        return {};
    }

    static clearLogin() {
        sessionStorage.clear();
    }
}

export default UserStorage;

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值