Skip to content
一、针对单组件多请求的Loading处理

场景:现在的后端基本都是微服务模式,相关的大屏项目,需要有个接口收集的地方,一般做法是需要有个 bff 应用[成本高]。 如果没有,前端大屏如果想加上 Loading,不易实现,请求场景多,有的是业务组件中发起的请求,有的是在 vuex 的 actions中发起请求。

解决方案:针对大屏单组件多发请求场景,前端采用模块请求计数的方式,相关模块发起请求,请求数 + 1,相关模块请求响应完成,请求数 - 1。 当相关模块内请求开始的时候,Loading开启,相关模块请求完成,Loading 关闭。

1. Loading组件[基于 Ant Design of Vue 实现,可替换]
vue
<template>
  <a-spin :spinning="visible"  wrapperClassName="loading-wrap">
    <div v-if="visible" class="loading-wrap"></div>
  </a-spin>
</template>

<script>
import { Spin } from "ant-design-vue";
export default {
  components: {
    [Spin.name]: Spin,
  },
  data() {
    return {
      visible: true,
    };
  },
};
</script>
<style lang="less" scoped>
.loading-wrap {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
  //background: rgba(255, 255, 255, 0.1);
  z-index: 44;
}
.loading-box {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 100px;
  transform: translate(-50%, -50%);
  line-height: 28px;
  i {
    font-size: 24px;
    margin-right: 5px;
  }
}
</style>
  • 采用前端 session 存储相应模块请求计数

    js
    /**
     * 使用方法:
     * Local为本地缓存,Session为会话缓存
     * import { Local, Session } from '@/utils/storage'
     */
    
    const EXPIRE_KEY = '__expires';
    // 无限时间
    const InfinityTime = -1;
    
    class Storage {
    
      constructor(storageType) {
        this.storageType = storageType
      }
      /**
       * 存值,这里注意无需进行Object的转换,存什么都可以,方法内部会自动做类型转换
       * @param {*} key 存储key
       * @param {*} value 存储值
       * @param {*} time 有效时间,默认为永久
       */
      set(key, value, time = InfinityTime) {
        const data = {
          [EXPIRE_KEY]: time === InfinityTime ? time : Date.now() + time,
          value
        };
        global[this.storageType].setItem(key, JSON.stringify(data))
      }
    
      /**
       * 是否存在
       * @param key
       * @return {boolean}
       */
      has(key) {
        return this.get(key) !== null
      }
    
      /**
       * 取值,这里如果是JsonObject类型会自动帮你转回原类型
       * 取值后无需再多做一次类型转换
       * @param {*} key
       * @param {*} defaultVal 默认值
       */
      get(key, defaultVal = null) {
        const data = global[this.storageType].getItem(key);
        if (data) {
          const parsedData = JSON.parse(data);
          const expireTime = parsedData[EXPIRE_KEY];
          if (expireTime === InfinityTime || expireTime >= Date.now()) {
            return parsedData.value
          }
          // 过期则清空对应key数据
          this.del(key)
        }
        return defaultVal
      }
    
      del(key) {
        global[this.storageType].removeItem(key)
      }
    
      clear() {
        global[this.storageType].clear()
      }
    }
    
    export const Local = new Storage('localStorage')
    export const Session = new Storage('sessionStorage')
  • 模块请求标记[基于 axios 处理]

    • 优雅的方式,通过 options 参数进行传递,如果采用这种方式的话,那就需要请求返回需要统一包一下,需要将 options 参数传进去,否则是拿不到 options 中的参数的。
    • 粗暴的方式,就在请求 header 中添加业务参数,缺点就是污染了请求头。[本篇采用方式]
      • 请求拦截
        javascript
        if (config.biz) {
          config.headers.biz = config.biz;
          // 1. 先获取
          const requestCount = Session.get(config.biz) || 0;
          Session.set(config.biz, requestCount + 1)
        }
      • 响应拦截
        javascript
        const { config } = response;
        if (config?.biz) {
          // 1. 先获取
          const requestCount = Session.get(config?.biz) || 0;
          Session.set(config.biz, requestCount - 1);
        }
      • 模块请求定义
        javascript
          /**
          * 全量用户弹框
          * @param params
          * @return {Promise<{data: {Message: string, Data: {}, ErrCode: string}}>|*}
          */
          const qryCardInfo = (params) => {
            return nPost(`${baseUrl}/iopcmpcard/ipTrace/getCardInfo`, params, {}, { biz: 'baseInfo' });
          };
  • 指令

    • 功能:自动挂载/关闭 自定义 Loading 组件

    • 逻辑:相关业务请求开始,显示 Loading, 根据业务判断多个请求是否请求完毕,完成,则隐藏 Loading

    • 用法:v-multiple-loading="业务名称"

    js
    import Vue from "vue";
    import { Session } from "../../utils/Storage";
    import Loading from "@/views/components/Loading.vue";
    
    /**
     *  场景:现在的后端基本都是微服务模式,相关的大屏项目,需要有个接口收集的地方,一般做法是需要有个 bff 应用[成本高]。
     *  如果没有,前端大屏如果想加上 Loading,不易实现,请求场景多,有的是业务组件中发起的请求,有的是在 vuex 的 actions中发起请求。
     *  这边针对大屏单组件多发请求场景,自定义一个指令
     *  功能:自动挂载/关闭 自定义 Loading 组件
     *  逻辑:相关业务请求开始,显示 Loading
     *       根据业务判断多个请求是否请求完毕,完成,则隐藏 Loading
     *  用法:v-multiple-loading="业务名称"
     */
    const Mask = Vue.extend(Loading);
    
    // 更新是否显示
    const toggleLoading = (el, binding) => {
      if (binding.value) {
        Vue.nextTick(function () {
          const bizKey = binding.value;
          let timer = null;
          // 控制loading组件显示
          el.instance.visible = true;
          // 插入到目标元素
          insertDom(el, el, binding);
          timer = setInterval(() => {
            const count = Session.get(bizKey);
            if (count === 0) {
              const interval = Session.get(`${bizKey}Interval`);
              if (interval) {
                clearInterval(interval);
                Session.del(`${bizKey}Interval`);
                el.instance.visible = false;
              }
            }
          }, 1000);
          Session.set(`${bizKey}Interval`, timer);
        });
      } else {
        console.error('请输入对应的业务')
      }
    };
    
    // 插入到目标元素
    const insertDom = (parent, el) => {
      parent.appendChild(el.mask);
    };
    
    const multipleLoading = Vue.directive("multipleLoading", {
      //第一次绑定到元素时调用
      bind: function (el, binding) {
        el.style.position = "relative";
        const mask = new Mask({
          el: document.createElement("div"),
          data() {},
        });
        //用一个变量接住mask实例
        el.instance = mask;
        el.mask = mask.$el;
        el.maskStyle = {};
        toggleLoading(el, binding);
      },
      //指令与元素解绑时调用
      unbind: function (el) {
        el.style.position = "";
        el.instance && el.instance.$destroy();
      },
    });
    
    export default { multipleLoading };
  • 防止浏览器意外关闭或者多次快速刷新导致请求计数未成功清0,监听每次浏览器刷新事件,清空 session

    javascript
    // 监听浏览器是否关闭
    window.addEventListener( "beforeunload", e => {
      // 每次浏览器刷新,清空session的值
      Session.clear();
    });

Released under the MIT License.