在 vue 项目中,我们通常会使用 Axios 库来与后台进行数据交互。而当我们发起 ajax 请求时,常常需要在页面上显示一个加载框(Loading 效果),然后等数据返回后自动将其隐藏。要实现这个功能,我们可以在每次请求前手动显示个加载框,等收到数据后又将其隐藏。但如果每个请求要都这么做,就略显麻烦。
下面通过样例演示如何封装一个带 loading 效果的 Axios 组件,它能够对请求和响应进行拦截从而实现 loading 的自动显示与隐藏,并且在请求失败时自动弹出消息提示框显示错误信息。
1、组件封装
(1)在项目中创建一个 http.js,里面内容是对 Axios 进行二次封装,代码如下:
代码说明:
基本原理是通过axios提供的请求拦截和响应拦截的接口,控制loading的显示或者隐藏。同时在请求失败时还会自动弹出消息提示框显示错误信息。
loding效果这里采用的是Element UI中提供的Loading组件来实现。而错误消息提示框则用的是Element UI中的Message组件来实现。
内部有个计数器,确保同一时刻如果有多个请求的话,不会同时出现多个loading,而是只有1个。并且在所有请求结束后才会隐藏loading。
使用了lodash的debounce防抖。因为有时会碰到在一次请求完毕后又立刻又发起一个新的请求的情况(比如删除一个表格条目后立刻进行刷新)。这种情况会造成连续loading两次,并且中间有一次极短的闪烁。通过防抖可以让300ms间隔内的loading便合并为一次,避免闪烁的情况。
默认所有请求都会自动有loading效果。如果某个请求不需要loading效果,可以在请求header中showLoading设置为false。
默认的loading效果时全屏的(覆盖在body上)。如果某个请求是需要在某个指定元素上显示loading效果,可以将请求header中loadingTarget设置为该元素的选择符。
// according to https://www.hangge.com/blog/cache/detail_2554.html
import axios from 'axios'
// import { ElMessage, ElLoading } from 'element-plus' 重复引用会导致样式失效
import _ from 'lodash'
const http = axios.create({
baseURL: '/api', //设置请求的base url
timeout: 10000, // 10s 超时时长
})
//loading对象
let loading
//当前正在请求的数量
let needLoadingRequestCount = 0
//显示loading
function showLoading(target) {
// 后面这个判断很重要,因为关闭时加了抖动,此时loading对象可能还存在,
// 但needLoadingRequestCount已经变成0.避免这种情况下会重新创建个loading
if (needLoadingRequestCount === 0 && !loading) {
loading = ElLoading.service({
lock: true,
text: '加载中...',
background: 'rgba(255, 255, 255, 0.5)',
target: target || 'main',
})
}
needLoadingRequestCount++
}
//隐藏loading
function hideLoading() {
needLoadingRequestCount = Math.max(--needLoadingRequestCount, 0) //做个保护
if (needLoadingRequestCount === 0) {
toHideLoading() //关闭loading
}
}
// 防抖:将 300ms 间隔内的关闭 loading 便合并为一次
var toHideLoading = _.debounce(() => {
if (loading) {
loading.close();
loading = null; // 释放资源
}
}, 300)
//添加请求拦截器
http.interceptors.request.use(
(request) => {
//判断当前请求是否设置了不显示Loading
if (request.headers.showLoading !== false) {
showLoading(request.headers.loadingTarget)
}
return request
},
(err) => {
//判断当前请求是否设置了不显示Loading
if (request.headers.showLoading !== false) {
hideLoading()
}
ElMessage.error('请求超时,请重试!')
return Promise.resolve(err)
},
)
//响应拦截器
http.interceptors.response.use(
(response) => {
// console.log(response.data)
if (!response.data.success) {
ElMessage.error(response.data.message)
}
//判断当前请求是否设置了不显示Loading(不显示自然无需隐藏)
if (response.config.headers.showLoading !== false) {
hideLoading()
}
return response
},
(error) => {
//判断当前请求是否设置了不显示Loading(不显示自然无需隐藏)
if (error.config.headers.showLoading !== false) {
hideLoading()
}
if (error.response && error.response.data && error.response.data.message) {
var jsonObj = JSON.parse(error.response.data.message)
ElMessage.error(jsonObj.message)
} else {
ElMessage.error(error.message)
}
return Promise.reject(error)
},
)
export default http
(2)接着在main.js 中将该组件引入,并定义成原型属性方便使用。
import http from './services/loading.js'
// 请求
app.config.globalProperties.$axios = http
2、使用样例
(1)正常情况下,每次发起请求页面上都会显示一个全屏的loading效果。
this.$axios.get("/api/menu")
.then(response => {
this.menus = response.data;
})
(3)如果将请求 header 中 loadingTarget设置为页面上某个元素的选择符,则 loading效果会只显示在个元素区域上。比如我们下面代码我们只让 loading遮罩显示在弹出框的内容区域上。
this.$axios.get("/api/controlApp?method=" + method, {headers: {'loadingTarget': '#dialogContent'}})
.then(response => {
// 重新加载数据
this.loadAppData();
ElMessage.success('执行成功!');
})