一、针对单组件多请求的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="业务名称"
jsimport 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(); });