2 Star 28 Fork 10

kevin / 中后台管理系统

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
21axios多请求合并一次loading功能封装.md 23.59 KB
一键复制 编辑 原始数据 按行查看 历史
kevin 提交于 2022-02-22 17:21 . 重复请求处理

axios封装多个请求合并一次loading

封装loading事件

src/utils文件夹下新建loading.ts文件:

/**
 * 全局loading效果:合并多次loading请求,避免重复请求
 * 当调用一次startLoading,则次数+1;当次数为0时,则显示loading
 * 当调用一次endLoading,则次数-1; 当次数为0时,则结束loading
 */
import { ElLoading } from 'element-plus';
import _ from 'lodash';
import { nextTick } from 'vue';
 
// 定义一个请求次数的变量,用来记录当前页面总共请求的次数
let loadingRequestCount = 0;
// 初始化loading实例
let loadingInstance;
  
 // 编写一个显示loading的函数 并且记录请求次数 ++
const startLoading = () => {
     if (loadingRequestCount === 0) {
         // 以服务的方式调用loading,默认是全屏loading,这里也可以继续封装自定义传参,控制loading的样式,具体参数见element-plus的loading组件
         loadingInstance = ElLoading.service();
     }
     loadingRequestCount++
 }
  
 // 编写一个隐藏loading的函数,并且记录请求次数 --
const endLoading = () => {
    // 以服务的方式调用的 Loading 需要异步关闭
    nextTick(()=>{
        loadingRequestCount--;
        loadingRequestCount = Math.max(loadingRequestCount, 0); // 保证大于等于0
        if (loadingRequestCount === 0) {
            if(loadingInstance){
                hideLoading();
            }
        }
   });
}

// 防抖:将 300ms 间隔内的关闭 loading 合并为一次。防止连续请求时, loading闪烁的问题。
// 因为有时会碰到在一次请求完毕后又立刻又发起一个新的请求的情况(比如删除一个表格行后立刻进行刷新)
// 这种情况会造成连续 loading 两次,并且中间有一次极短的闪烁。通过防抖可以让 300ms 间隔内的 loading 合并为一次,避免闪烁的情况。
var hideLoading = _.debounce(() => {
  loadingInstance.close();
  loadingInstance = null;
}, 300);

export {
    startLoading,
    endLoading
}

axios调用loading组件

修改src/utils/http/index.ts文件中的请求拦截与响应拦截:

import { startLoading, endLoading } from '@/utils/loading';
import { ElMessageBox } from 'element-plus';

// 请求拦截
private httpInterceptorsRequest(): void {
    AxiosHttp.axiosInstance.interceptors.request.use(
        (config: AxiosHttpRequestConfig) => {
        startLoading();
        // 断网提示
        if (!navigator.onLine) {
            ElMessageBox.alert(
            '您的网络故障,请检查!',
            '温馨提示',
            {
                confirmButtonText: '好的',
                type: 'warning'
            }
            )
        }
        const $config = config;
        // 开启进度条动画
        NProgress.start();
        // 优先判断请求是否传入了自定义配置的回调函数,否则执行初始化设置等回调
        if (typeof config.beforeRequestCallback === "function") {
            config.beforeRequestCallback($config);
            this.beforeRequestCallback = undefined;
            return $config;
        }
        // 判断初始化状态中有没有回调函数,没有的话
        if (AxiosHttp.initConfig.beforeRequestCallback) {
            AxiosHttp.initConfig.beforeRequestCallback($config);
            return $config;
        }
        // 确保config.url永远不会是undefined,增加断言
        if(!config.url){
            config.url = ""
        }
        // 登录接口和刷新token接口不需要在headers中回传token,在走刷新token接口走到这里后,需要拦截,否则继续往下走,刷新token接口这里会陷入死循环
        if (config.url.indexOf('/refreshToken') >= 0 || config.url.indexOf('/login') >= 0) {
            return $config
        }
        // 回传token
        const token = getToken();
        // 判断token是否存在
        if (token) {
            const data = JSON.parse(token);
            // 确保config.headers永远不会是undefined,增加断言
            if(!config.headers){
            config.headers = {}
            }
            config.headers["Authorization"] = "Bearer " + data.accessToken;
            return $config;
        } else {
            return $config;
        }
        },
        error => {
        // 当前请求出错,当前请求加1的loading也需要减掉
        endLoading();
        return Promise.reject(error);
        }
    );
}

// 响应拦截
private httpInterceptorsResponse(): void {
    const instance = AxiosHttp.axiosInstance;
    instance.interceptors.response.use(
        (response: AxiosHttpResponse) => {
        console.log(response,"请求响应数据");
        const $config = response.config;
        // 关闭进度条动画
        NProgress.done();
        if(response.data.code==200){
            // 优先判断请求是否传入了自定义配置的回调函数,否则执行初始化设置等回调
            if (typeof $config.beforeResponseCallback === "function") {
            $config.beforeResponseCallback(response);
            this.beforeResponseCallback = undefined;
            return response.data;
            }
            if (AxiosHttp.initConfig.beforeResponseCallback) {
            AxiosHttp.initConfig.beforeResponseCallback(response);
            return response.data;
            }
        }else{
            if(response.data.code==201002){
            errorMessage("登录已超时,请重新登录!");
            // 清除cookie中的token
            removeToken();
            // 清除缓存中的用户信息
            storageSession.removeItem("userInfo");
            router.push(`/Login?redirect=${router.currentRoute.value.fullPath}`);
            }
            if(response.data.code==201004){
            const token = getToken();
            if(token){
                const data = JSON.parse(token);
                if(!isRefreshing){
                isRefreshing = true;
                return refreshToken({refreshToken:data.refreshToken}).then((res:any)=>{
                    if(res.status){
                    setToken(res.data);
                    // 确保config.headers永远不会是undefined,增加断言
                    if(!$config.headers){
                        $config.headers = {}
                    }
                    $config.headers['Authorization'] = "Bearer " + res.data.accessToken;
                    $config.baseURL = ''
                    // 已经刷新了token,将所有队列中的请求进行重试
                    requests.forEach((callback:any) => callback(token))
                    requests = [];
                    return instance($config);
                    }
                }).catch((res:any)=>{
                    // 清除cookie中的token
                    removeToken();
                    // 清除缓存中的用户信息
                    storageSession.removeItem("userInfo");
                    router.push(`/Login?redirect=${router.currentRoute.value.fullPath}`);
                }).finally(()=>{
                    isRefreshing = false;
                });
                }else{
                // 正在刷新token,将返回一个未执行resolve的promise
                return new Promise((resolve) => {
                    // 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行
                    requests.push((accessToken:string) => {
                    // 响应以后,请求接口的$config.url已经包含了baseURL部分,这里需要将baseURL清空,防止再次请求时,再次组装,导致url错误
                    $config.baseURL = '';
                    // 确保config.headers永远不会是undefined,增加断言
                    if(!$config.headers){
                        $config.headers = {}
                    }
                    $config.headers['Authorization'] = "Bearer " + accessToken;
                    resolve(instance($config));
                    })
                })
                }
            }
            }
        }
        endLoading();
        return response.data;
        },
        (error) => {
        const $error = error;
        endLoading();
        // 关闭进度条动画
        NProgress.done();
        // 请求超时处理
        if ($error.message.indexOf('timeout') !== -1) {
            ElMessageBox.alert(
            '请求超时,请重新刷新页面!',
            '温馨提示',
            {
                confirmButtonText: '好的',
                type: 'warning'
            }
            )
        }
        // 所有的响应异常 区分来源为取消请求/非取消请求
        return Promise.reject($error);
        }
    );
}

以上对全局loading封装就完成了。这种封装后,整个项目就必须都使用loading了,不想使用也只能所有页面都不使用,在封装的axios中注释掉loading相关代码,如果想实现部分页面不使用这个全局loading,也可以对请求和loading再进行改造。

实现页面可控是否使用全局loading

实现控制某个页面是否使用全局loading,需要改造接口,其原理是通过接口传递参数,在请求拦截与响应拦截中通过判断该参数,来决定当前接口是否使用全局loading,所以这种形式是针对请求的,如果一个页面同时有多个请求产生,建议最好全部统一处理,如果只针对其中某一个不走全局loading,就会有冲突出现,不走全局loading的页面内,所有请求对于loading的处理就需要统一;另外动态获取菜单数据的请求不属于某个页面,而是框架层的请求,所以,如果必须有这方面的需求,动态获取菜单的请求最好默认设置不使用全局loading

  1. 修改src/views/News/Index/index.vue文件:
function getInitData(){
  getNewsListData({config:{showLoading:false}}).then((res:any)=>{
    if(res.data&&res.data.length>0){
      initData.push(...res.data);
    }
  });
}
  1. 这里给api传一个参数配置,然后修改对应的api接口,src/api/news.ts:
// 获取新闻列表
export const getNewsListData = (config:object) => {
  return http.request("post", "/getNewsList",null,config);
};
  1. axios封装时有说到,封装的axios会接收4个参数,请求类型、请求链接、请求需要的参数、请求配置参数,这里需要在请求配置参数中传值,第三个参数也需要带上才能一一对应,这里就需要修改axios封装方法了,修改src/utils/http/index.ts文件:
  
  // 请求拦截
  private httpInterceptorsRequest(): void {
    AxiosHttp.axiosInstance.interceptors.request.use(
      (config: AxiosHttpRequestConfig) => {
        const $config = config;
        console.log($config.config,"请求拦截");
        if($config.config.showLoading){
          startLoading();
        }
        // 断网提示
        if (!navigator.onLine) {
          ElMessageBox.alert(
            '您的网络故障,请检查!',
            '温馨提示',
            {
              confirmButtonText: '好的',
              type: 'warning'
            }
          )
        }
        // 开启进度条动画
        NProgress.start();
        // 优先判断请求是否传入了自定义配置的回调函数,否则执行初始化设置等回调
        if (typeof config.beforeRequestCallback === "function") {
          config.beforeRequestCallback($config);
          this.beforeRequestCallback = undefined;
          return $config;
        }
        // 判断初始化状态中有没有回调函数,没有的话
        if (AxiosHttp.initConfig.beforeRequestCallback) {
          AxiosHttp.initConfig.beforeRequestCallback($config);
          return $config;
        }
        // 确保config.url永远不会是undefined,增加断言
        if(!config.url){
          config.url = ""
        }
        // 登录接口和刷新token接口不需要在headers中回传token,在走刷新token接口走到这里后,需要拦截,否则继续往下走,刷新token接口这里会陷入死循环
        if (config.url.indexOf('/refreshToken') >= 0 || config.url.indexOf('/login') >= 0) {
          return $config
        }
        // 回传token
        const token = getToken();
        // 判断token是否存在
        if (token) {
          const data = JSON.parse(token);
          // 确保config.headers永远不会是undefined,增加断言
          if(!config.headers){
            config.headers = {}
          }
          config.headers["Authorization"] = "Bearer " + data.accessToken;
          return $config;
        } else {
          return $config;
        }
      },
      error => {
        // 当前请求出错,当前请求加1的loading也需要减掉
        if(error.config.config.showLoading){
          endLoading();
        }
        return Promise.reject(error);
      }
    );
  }

  // 响应拦截
  private httpInterceptorsResponse(): void {
    const instance = AxiosHttp.axiosInstance;
    instance.interceptors.response.use(
      (response: AxiosHttpResponse) => {
        console.log(response,"请求响应数据");
        const $config = response.config;
        // 关闭进度条动画
        NProgress.done();
        if(response.data.code==200){
          // 优先判断请求是否传入了自定义配置的回调函数,否则执行初始化设置等回调
          if (typeof $config.beforeResponseCallback === "function") {
            $config.beforeResponseCallback(response);
            this.beforeResponseCallback = undefined;
            return response.data;
          }
          if (AxiosHttp.initConfig.beforeResponseCallback) {
            AxiosHttp.initConfig.beforeResponseCallback(response);
            return response.data;
          }
        }else{
          if(response.data.code==201002){
            errorMessage("登录已超时,请重新登录!");
            // 清除cookie中的token
            removeToken();
            // 清除缓存中的用户信息
            storageSession.removeItem("userInfo");
            router.push(`/Login?redirect=${router.currentRoute.value.fullPath}`);
          }
          if(response.data.code==201004){
            const token = getToken();
            if(token){
              const data = JSON.parse(token);
              if(!isRefreshing){
                isRefreshing = true;
                return refreshToken({refreshToken:data.refreshToken}).then((res:any)=>{
                  if(res.status){
                    setToken(res.data);
                    // 确保config.headers永远不会是undefined,增加断言
                    if(!$config.headers){
                      $config.headers = {}
                    }
                    $config.headers['Authorization'] = "Bearer " + res.data.accessToken;
                    $config.baseURL = ''
                    // 已经刷新了token,将所有队列中的请求进行重试
                    requests.forEach((callback:any) => callback(token))
                    requests = [];
                    return instance($config);
                  }
                }).catch((res:any)=>{
                  // 清除cookie中的token
                  removeToken();
                  // 清除缓存中的用户信息
                  storageSession.removeItem("userInfo");
                  router.push(`/Login?redirect=${router.currentRoute.value.fullPath}`);
                }).finally(()=>{
                  isRefreshing = false;
                });
              }else{
                // 正在刷新token,将返回一个未执行resolve的promise
                return new Promise((resolve) => {
                  // 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行
                  requests.push((accessToken:string) => {
                    // 响应以后,请求接口的$config.url已经包含了baseURL部分,这里需要将baseURL清空,防止再次请求时,再次组装,导致url错误
                    $config.baseURL = '';
                    // 确保config.headers永远不会是undefined,增加断言
                    if(!$config.headers){
                      $config.headers = {}
                    }
                    $config.headers['Authorization'] = "Bearer " + accessToken;
                    resolve(instance($config));
                  })
                })
              }
            }
          }
        }
        if(response.config.config.showLoading){
          endLoading();
        }
        return response.data;
      },
      (error) => {
        const $error = error;
        if($error.config.config.showLoading){
          endLoading();
        }
        // 关闭进度条动画
        NProgress.done();
        // 请求超时处理
        if ($error.message.indexOf('timeout') !== -1) {
          ElMessageBox.alert(
            '请求超时,请重新刷新页面!',
            '温馨提示',
            {
              confirmButtonText: '好的',
              type: 'warning'
            }
          )
        }
        // 所有的响应异常 区分来源为取消请求/非取消请求
        return Promise.reject($error);
      }
    );
  }

  // 通用请求工具函数
  public request<T>(
    method: RequestMethods,
    url: string,
    param?: AxiosRequestConfig,
    axiosConfig?: AxiosHttpRequestConfig
  ): Promise<T> {
    // post与get请求的参数需要用不同的key去接收,这里需要做判断,在请求中get的参数是params接收的,post的参数是data接收的
    const config = AxiosHttp.transformConfigByMethod(param, {
      method,
      url,
      ...axiosConfig
    } as AxiosHttpRequestConfig);
    // 如果没有自定义的loading配置,需要给默认值,默认值就是启用全局loading
    if(config.config===undefined){
      config.config={
        showLoading:true
      };
    }else{ // 防止定义了config,但未定义showLoading
      if(config.config.showLoading===undefined){
        config.config.showLoading = true;
      }
    }
    // 单独处理自定义请求/响应回调
    if (axiosConfig?.beforeRequestCallback) {
      this.beforeRequestCallback = axiosConfig.beforeRequestCallback;
    }
    if (axiosConfig?.beforeResponseCallback) {
      this.beforeResponseCallback = axiosConfig.beforeResponseCallback;
    }
    // 单独处理自定义请求/响应回掉
    return new Promise((resolve, reject) => {
      AxiosHttp.axiosInstance.request(config).then((response:any) => {
        resolve(response);
      }).catch(error => {
        reject(error);
      });
    });
  }

以上对封装请求,以及请求拦截、响应拦截,都有部分代码修改,这里的loading计数在开始与结束,都需要判断当前请求是否启用全局loading,并且在请求发生错误时,也一样需要判断,这样才能保证计数的准确,某个请求未启用全局loading那么它的请求拦截不参与loading次数的递增,响应拦截应同样需要不参与loading次数的递减,请求错误、响应错误内也是同样的道理。 4. 这里给axios请求添加了自定义参数配置,需要添加对应的类型定义,修改src/utils/http/types.d.ts文件:

// 定义自定义回调中请求的数据类型
export interface AxiosHttpRequestConfig extends AxiosRequestConfig {
  config?:{
    showLoading?:boolean;
  };
  beforeRequestCallback?: (request: AxiosHttpRequestConfig) => void; // 请求发送之前
  beforeResponseCallback?: (response: AxiosHttpResponse) => void; // 相应返回之前
}

此时运行项目,打开新闻列表页,该页面就不会启用loading,打开一个新闻详情页,页面正常调用全局loading跳回新闻列表页,使用浏览器刷新,还会调用全局loading,这就是上面说到的框架层的获取菜单路由数据的接口,其默认是开启全局loading的,不想要loading的话,获取菜单路由数据的接口,就需要默认传参设置为不调用全局loading

自定义loading样式

以服务的方式调用loadingloading内可以传递参数,具体参数可参考element-plusloading组件,这里可以先给一个默认样式,修改src/utils/loading.ts文件:

 // 编写一个显示loading的函数 并且记录请求次数 ++
const startLoading = () => {
     if (loadingRequestCount === 0) {
         // 以服务的方式调用loading,默认是全屏loading,这里也可以继续封装自定义传参,控制loading的样式,具体参数见element-plus的loading组件
         let options = {
            lock: true,
            text: 'Loading...',
            background: 'rgba(0, 0, 0, 0.7)',
          }
         loadingInstance = ElLoading.service(options);
     }
     loadingRequestCount++
 }

此时运行项目,全局loading的样式就变了,也可以通过请求传递参数,自定义当前请求的的loading样式,其逻辑与自定义是否启用全局loading一样,先将loading组件的配置参数在类型定义中全部加上,修改src/utils/http/types.d.ts文件:

// 定义自定义回调中请求的数据类型
export interface AxiosHttpRequestConfig extends AxiosRequestConfig {
  config?:{
    showLoading?:boolean;
    loadingStyle?:{
      target:object|string;
      body:boolean;
      fullscreen:boolean;
      lock:boolean;
      text:string;
      spinner:string;
      background:string;
      'custom-class':string;
    }
  };
  beforeRequestCallback?: (request: AxiosHttpRequestConfig) => void; // 请求发送之前
  beforeResponseCallback?: (response: AxiosHttpResponse) => void; // 相应返回之前
}

然后修改src/utils/http/index.ts文件的请求拦截中的startLoading:

if($config.config.showLoading){
  startLoading($config.config.loadingStyle);
}

最后修改src/utils/loading.ts文件的startLoading方法:

 // 编写一个显示loading的函数 并且记录请求次数 ++
const startLoading = (opt) => {
     if (loadingRequestCount === 0) {
         // 以服务的方式调用loading,默认是全屏loading,这里也可以继续封装自定义传参,控制loading的样式,具体参数见element-plus的loading组件
         let options = {
            lock: true,
            text: 'Loading...',
            background: 'rgba(0, 0, 0, 0.7)',
          }
          if(opt){
              options = opt;
          }
         loadingInstance = ElLoading.service(options);
     }
     loadingRequestCount++
}

此时loading组件的样式自定义参数已经加上,可以在接口请求时设置自定义样式,来改变全局loading的默认样式。修改src/views/News/NewsDetail/index.vue文件:

// 获取初始化详情信息
function getInitData(currentID){
  getNewsDetailData({id:currentID},{config:{showLoading:true,loadingStyle:{lock:false,text:"加载中...",background:'rgba(255,255,255,.7)'}}}).then((res:any)=>{
    if(res.data){
      initData.data = res.data;
      console.log(initData);
    }
  });
}

然后修改src/api/news.ts文件中的获取详情接口:

// 获取新闻详情
export const getNewsDetailData = (data:object,config:object) => {
  return http.request("post", "/getNewsDetail",data,config);
};

此时运行项目,从侧边栏打开新闻列表页,再打开一个详情页,可以发现,详情页的loading变为了白色背景,此时如使用浏览器刷新,可以看到分别有一次黑色和白色背景loading出现,这与上文说过的全局loading是否启用是相同的问题。同一个页面内的自定义loading的配置必须保持一致。如果真有需求各页面loading样式有不同需求,或者有需求某些页面不需要加载loading,那么框架层上的获取菜单数据接口是否调用全局loading就是需要考虑的问题了。

马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
1
https://gitee.com/ctokevin/vue-admin-system.git
git@gitee.com:ctokevin/vue-admin-system.git
ctokevin
vue-admin-system
中后台管理系统
main

搜索帮助

344bd9b3 5694891 D2dac590 5694891