怎样统一添加loading
背景
页面加载普遍需要一个加载中的过程,简单直接的方案就是在每个页面单独添加,但是确实很冗余。所以能不能统一添加呢?
思路
loading的过程说白了就是接口请求的过程,所以大体思路就是我们可以在请求拦截器中做处理,请求时loading,结束时取消loading,完美~
干货 - 直接上核心代码(此处用vue3 + Element Plus举例说明)
1. store
import { reactive } from 'vue';
import { defineStore } from 'pinia';
type pageDataType = {
requestingObj?: any;
route?: any;
};
// page store
export const usePage = defineStore('page', () => {
// 请求数据
const pageData = reactive<pageDataType>({
requestingObj: {}, // 正在请求的接口集合,若存在正在请求的接口,则loading
route: {}, // 路由信息,用于获取当前页面加载loading的dom,即meta.loadingContainerDom
});
/**
* 更新请求数据
* @param newData { pageDataType }
*/
const updatePageData = (newData: pageDataType) => {
if (newData.requestingObj)
pageData.requestingObj = newData.requestingObj;
if (newData.route) pageData.route = newData.route;
};
return { pageData, updatePageData };
});
2. axios
拦截器:
import { ElLoading } from 'element-plus';
import { usePage } from '@/stores/page';
import type {
CreateAxiosDefaults,
AxiosInstance,
AxiosRequestConfig,
} from 'axios';
type AxiosRequestType = AxiosRequestConfig & {
loading?: boolean; // 加载页面loading
};
// 显示loading
let loadingInstance: any = null;
let loadingTimer: number = 0;
const showLoading = (target: string = 'body') => {
// 加一个延迟,若加载时间小于100ms,则显示loading,算是一个小优化,也可以不要延迟
loadingTimer = window.setTimeout(() => {
if (loadingTimer && !loadingInstance) {
loadingInstance = ElLoading.service({
target,
background: '#ccc',
});
}
}, 100);
};
// 关闭loading
const closeLoading = () => {
loadingTimer && window.clearTimeout(loadingTimer);
loadingTimer = 0;
loadingInstance && loadingInstance.close();
loadingInstance = null;
};
// 请求拦截
axios.interceptors.request.use((config: AxiosRequestType) => {
const { pageData, updatePageData } = usePage();
const { requestingObj, route } = pageData;
// 判断如果是需要loading的请求,则在requestingObj中添加此数据
if (config.loading) {
const key = config.method + (config.url || ''); // 这里的key命名方式可能不太严谨,有可能不唯一,但是为了后边为了方便删除此项,所以暂且先如此命令
requestingObj[key] = true;
updatePageData({ requestingObj });
}
// 如果有需要loading的请求,则loading
if (JSON.stringify(requestingObj) !== '{}') {
showLoading(route.meta.loadingContainerDom);
}
return config;
});
// 响应拦截
axios.interceptors.response.use(
(res: any) => {
const { config } = res;
const { pageData, updatePageData } = usePage();
const { requestingObj }: any = pageData;
const key = config.method + config.url;
// 如果需要loading的请求数为空,则不再loading
if (config.loading && requestingObj[key]) {
delete requestingObj[key];
updatePageData({ requestingObj });
}
if (JSON.stringify(requestingObj) === '{}') {
closeLoading();
}
},
);
api
axios.get('/report/info', {
loading: true,
})
3. 路由
拦截器
import { usePage } from '../stores/page';
router.afterEach((to, from, failure) => {
// 切换页面之后清空此页面的loading,另外更新路由信息 - 为了获取route.meta.loadingContainerDom
const { updatePageData } = usePage();
updatePageData({ requestingObj: {}, route: to });
return true;
});
路由配置
...
// 在meta标签中添加loadingContainerDom
{
name: 'report',
path: 'report',
component: ReportList,
meta: {
loadingContainerDom: '.base-layout_main',
},
},
...
思路/解释
-
首先是全局状态管理store:
1.1 因为页面加载不一定只有一个接口,所以要把需要loading的请求全局保存一下 - requestingObj
1.2 loading是个遮罩,如果直接加在整个页面上,我想切换页面的话前提是这个页面的loading结束才能切换,体验效果差,故loading加在哪里?需要指定dom,所以我们可以通过在路由配置中添加此dom,因此就有了另一个变量 - route
这里为什么不直接在使用route的地方(axios拦截器)直接用useRoute方法直接获取呢?具体方法如下:
import { useRoute } from 'vue-router'; const route = useRoute();这不就有了么?!
因为useRoute必须在
setup()中使用,所以这里用store中转了一下(官网说明),store的存储是在router.afterEach中处理的。 -
然后是axios:
2.1 拦截器的逻辑就和我们最开始说的思路是一致的,请求拦截中将若存在需要loading的请求,则显示loading,另外需要将其存一下,因为有可能,很有可能不止一个请求,所以需要在请求结束后(响应拦截)将其删掉,直到所有请求都结束才取消loading
2.2 api中将需要loading的请求添加loading字段
-
最后是路由:
3.1 拦截器中,在页面切换后取消所有loading,此时也需要更新route信息,为了获取loading的容器
route.meta.loadingContainerDom3.2 路由配置中在meta标签中添加容器
loadingContainerDom
完~