p>做项目时,加载状态没处理好特影响体验,比如页面切换、数据请求时用户盯着空白页干等。Vue3 里咋搞全局 Loading 呢?这篇从基础用法到进阶优化,把思路和实操步骤拆明白,新手也能跟着做
全局 Loading 基础实现:从组件到状态管理
要做全局 Loading,核心是一个能覆盖全页面的组件 + 统一的状态管理,先从最基础的结构搭起来。
先搞个全局 Loading 组件
全局 Loading 得“浮”在所有页面之上,用 <Teleport> 把组件挂载到 body 最安全(避免被父级样式干扰),组件逻辑很简单:控制显示/隐藏。
<!-- components/GlobalLoading.vue -->
<template>
<Teleport to="body">
<div class="global-loading" v-if="isShow">
<div class="loading-spinner">加载中...</div>
</div>
</Teleport>
</template>
<script setup>
import { defineProps } from 'vue'
defineProps({
isShow: Boolean // 外部传参控制显示
})
</script>
<style scoped>
.global-loading {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(255,255,255,0.8);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999; /* 层级要足够高 */
}
.loading-spinner {
/* 先简单写,后面加动画 */
}
</style>
用状态管理统一控制显示
Vue3 里推荐用 Pinia 做状态管理(Vuex 也能实现,Pinia 更轻量),创建一个专门的 loadingStore,管理 isLoading 状态和显示/隐藏方法。
// stores/loading.js
import { defineStore } from 'pinia'
export const useLoadingStore = defineStore('loading', {
state: () => ({
isLoading: false
}),
actions: {
show() {
this.isLoading = true
},
hide() {
this.isLoading = false
}
}
})
然后在 App.vue 里引入组件和 Store,让全局 Loading 跟着状态走:
<!-- App.vue -->
<template>
<GlobalLoading :isShow="loadingStore.isLoading" />
<router-view /> <!-- 项目的页面出口 -->
</template>
<script setup>
import GlobalLoading from './components/GlobalLoading.vue'
import { useLoadingStore } from './stores/loading'
const loadingStore = useLoadingStore()
</script>
路由切换时触发 Loading
页面跳转时,用户得看到加载反馈,用 Vue Router 的导航守卫,路由开始切换时显示 Loading,切换完成后隐藏。
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import { useLoadingStore } from '../stores/loading'
const router = createRouter({
history: createWebHistory(),
routes: [/* 你的路由配置 */]
})
// 路由开始前显示 Loading
router.beforeEach((to, from, next) => {
const loadingStore = useLoadingStore()
loadingStore.show()
next()
})
// 路由完成后隐藏 Loading
router.afterEach(() => {
const loadingStore = useLoadingStore()
loadingStore.hide()
})
结合 Axios 请求拦截,处理异步加载
路由切换的 Loading 解决了,但数据请求时的 Loading 更复杂(比如多个接口并发、请求失败要兜底),这时候得结合 Axios 拦截器,统计请求数量。
Axios 拦截器的核心逻辑
思路是:请求发起时计数+1,响应/失败时计数-1,当计数 > 0 时显示 Loading,计数 = 0 时隐藏,这样能处理“多个请求同时发起”的情况(比如页面初始化要调 3 个接口,全完成后再隐藏 Loading)。
// utils/request.js
import axios from 'axios'
import { useLoadingStore } from '../stores/loading'
const service = axios.create({
baseURL: import.meta.env.VITE_API_BASE, // 后端接口基础地址
timeout: 5000
})
let requestCount = 0 // 统计当前未完成的请求数
const loadingStore = useLoadingStore()
// 请求拦截器:发起请求时计数+1,计数为1时显示Loading
service.interceptors.request.use(
config => {
requestCount++
if (requestCount === 1) {
loadingStore.show()
}
return config
},
error => {
return Promise.reject(error)
}
)
// 响应拦截器:请求完成时计数-1,计数为0时隐藏Loading
service.interceptors.response.use(
response => {
requestCount--
if (requestCount === 0) {
loadingStore.hide()
}
return response
},
error => {
requestCount-- // 失败也要减计数,否则Loading会卡住
if (requestCount === 0) {
loadingStore.hide()
}
return Promise.reject(error)
}
)
export default service
路由与请求 Loading 的冲突处理
路由切换时,Loading 可能和请求 Loading 重叠(比如路由刚切完,请求还在加载),这时候要让 Loading 逻辑“统一”:把路由切换也当作一个“请求”来处理(或者延迟显示 Loading,避免闪烁)。
比如给 loadingStore 加个延迟显示逻辑:请求发起后,延迟 300ms 再显示 Loading,如果请求在 300ms 内完成,就不显示 Loading(避免“闪一下”的尴尬)。
修改 loadingStore:
// stores/loading.js
import { defineStore } from 'pinia'
export const useLoadingStore = defineStore('loading', {
state: () => ({
isLoading: false,
timer: null // 定时器ID,用于清除延迟
}),
actions: {
show() {
// 延迟300ms显示,避免快速请求闪烁
this.timer = setTimeout(() => {
this.isLoading = true
}, 300)
},
hide() {
clearTimeout(this.timer) // 清除延迟,避免定时器触发
this.isLoading = false
}
}
})
不同场景下的全局 Loading 适配
实际项目里,Loading 要应对路由切换、异步组件加载、组件内单独请求等场景,得针对性处理。
异步组件加载的 Loading
如果路由用 defineAsyncComponent 加载组件(比如大型组件分包加载),这时候组件加载也需要 Loading,可以在异步组件的 loader 里触发 Loading。
// router/index.js
import { createRouter, createWebHistory, defineAsyncComponent } from 'vue-router'
import { useLoadingStore } from '../stores/loading'
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/about',
component: defineAsyncComponent({
loader: async () => {
const loadingStore = useLoadingStore()
loadingStore.show() // 组件开始加载时显示
const module = await import('./views/AboutView.vue')
loadingStore.hide() // 组件加载完成后隐藏
return module
},
loadingComponent: LoadingPlaceholder // 可选:组件加载中的占位组件
})
}
]
})
组件内单独控制全局 Loading
比如某个按钮触发的大请求,需要显示全局 Loading,这时候直接调用 loadingStore 的方法,但要注意错误兜底(请求失败也要隐藏 Loading)。
<template>
<button @click="fetchBigData">获取大数据</button>
</template>
<script setup>
import { useLoadingStore } from '../stores/loading'
import request from '../utils/request'
const loadingStore = useLoadingStore()
const fetchBigData = async () => {
try {
loadingStore.show() // 显示全局Loading
const res = await request.get('/big-data')
// 处理数据...
} catch (error) {
console.error('请求失败:', error)
} finally {
loadingStore.hide() // 无论成功失败,都要隐藏
}
}
</script>
页面跳转与数据请求的联动
比如从列表页跳详情页,详情页的接口在 onMounted 里请求,这时候要让“路由切换 Loading”和“数据请求 Loading”连贯:
- 路由
beforeEach时显示 Loading(用户看到页面切换反馈)。 - 详情页
onMounted里发起请求,请求完成后隐藏 Loading。
但要注意:如果路由切换很快(100ms 完成),但请求要 2s,这时候 Loading 不能因为路由 afterEach 就提前隐藏。解决方案是:让数据请求的 Loading 逻辑“接管”路由的 Loading。
简单说,路由切换时的 Loading 只负责“页面跳转中”,数据请求的 Loading 负责“内容加载中”,两者逻辑分开,但体验上要连贯。
性能优化与避坑指南
全局 Loading 看似简单,实际要避很多坑:样式层级、重复触发、内存泄漏等。
避免 Loading 重复触发/卡死
- 请求计数要准确:Axios 拦截器里,
requestCount的增减要在所有分支执行(包括请求失败的回调),否则请求失败时计数没减,Loading 会一直显示。 - 定时器要清除:延迟显示的
timer,在hide时必须clearTimeout,否则多次触发会导致 Loading 失控。
样式层的体验优化
- 层级与遮罩:
z-index要足够大(9999),背景用半透明遮罩(rgba(255,255,255,0.8)),避免遮挡用户操作(虽然 Loading 时一般要禁止操作,但体验要柔和)。 - 加载动画:用 CSS 做旋转动画,或者引入 Lottie 做复杂动效,示例 CSS 动画:
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
服务端渲染(SSR)的兼容
如果项目用 Nuxt3 做 SSR,要注意:
<Teleport>在服务端渲染时可能报错,需要用process.client判断环境,只在客户端渲染 Loading 组件。- Store 实例要在客户端初始化,避免服务端和客户端状态不一致。
内存泄漏防范
如果在 Axios 拦截器里直接引用 loadingStore,可能因为 Store 实例没及时销毁导致内存泄漏。解决方案:在请求拦截时动态获取 Store 实例(确保每次请求都用最新的实例)。
进阶玩法:让全局 Loading 更智能
基础功能满足后,还能做这些优化,让 Loading 更“聪明”。
多状态区分:不同场景显示不同提示
比如区分“数据加载”和“资源加载”(如图片、文件),显示不同文案或动画,给 loadingStore 加个 loadingType 字段:
<!-- 修改 GlobalLoading.vue -->
<template>
<Teleport to="body">
<div class="global-loading" v-if="isShow">
<div class="loading-spinner">
<template v-if="loadingType === 'data'">数据加载中...</template>
<template v-else-if="loadingType === 'resource'">资源加载中...</template>
<template v-else>加载中...</template>
</div>
</div>
</Teleport>
</template>
<script setup>
import { defineProps } from 'vue'
defineProps({
isShow: Boolean,
loadingType: String
})
</script>
修改 Store,让 show 方法支持传参:
// stores/loading.js
export const useLoadingStore = defineStore('loading', {
state: () => ({
isLoading: false,
loadingType: 'default',
timer: null
}),
actions: {
show(type = 'default') {
this.loadingType = type
this.timer = setTimeout(() => {
this.isLoading = true
}, 300)
},
hide() {
clearTimeout(this.timer)
this.isLoading = false
this.loadingType = 'default'
}
}
})
触发时传类型:
// 数据请求时
loadingStore.show('data')
// 资源加载时(比如上传文件)
loadingStore.show('resource')
结合 Lottie 做复杂动画
用 Lottie 实现炫酷的加载动效(比如品牌化的动画),步骤:
- 用 AE 做动画,导出为 JSON 文件。
- 安装
lottie-web:npm i lottie-web。 - 在
GlobalLoading里渲染 Lottie 动画:
<template>
<Teleport to="body">
<div class="global-loading" v-if="isShow">
<div ref="lottieContainer" class="lottie-container"></div>
<p>{{ loadingText }}</p>
</div>
</Teleport>
</template>
<script setup>
import { onMounted, ref, watch, computed } from 'vue'
import lottie from 'lottie-web'
import animationData from '../assets/loading.json' // 动画JSON文件
const props = defineProps({
isShow: Boolean,
loadingType: String
})
const lottieContainer = ref(null)
let anim = null
// 监听isShow变化,控制动画播放/销毁
watch(() => props.isShow, (newVal) => {
if (newVal) {
anim = lottie.loadAnimation({
container: lottieContainer.value,
animationData,
loop: true,
autoplay: true
})
} else {
anim?.destroy() // 销毁动画,释放资源
}
})
// 根据loadingType显示不同文案
const loadingText = computed(() => {
if (props.loadingType === 'data') return '数据拼命加载中...'
if (props.loadingType === 'resource') return '资源火速加载中...'
return '加载ing...'
})
</script>
<style scoped>
.lottie-container {
width: 100px;
height: 100px;
}
</style>
封装成插件,全局调用更方便
把 Loading 逻辑封装成 Vue 插件,注册全局组件和方法,项目里用起来更丝滑。
// plugins/loading.js
import { createApp } from 'vue'
import GlobalLoading from '../components/GlobalLoading.vue'
import { useLoadingStore } from '../stores/loading'
export default {
install(app) {
// 注册全局组件
app.component('GlobalLoading', GlobalLoading)
// 提供全局方法(通过provide/inject)
app.provide('loading', {
show: (type) => {
const store = useLoadingStore()
store.show(type)
},
hide: () => {
const store = useLoadingStore()
store.hide()
}
})
// 也可以挂载到全局属性(this.$loading)
app.config.globalProperties.$loading = {
show: (type) => {
const store = useLoadingStore()
store.show(type)
},
hide: () => {
const store = useLoadingStore()
store.hide()
}
}
}
}
在 main.js 里注册插件:
import { createApp } from 'vue'
import App from './App.vue'
import loadingPlugin from './plugins/loading'
const app = createApp(App)
app.use(loadingPlugin)
app.mount('#app')
组件里调用(两种方式任选):
<template>
<button @click="handleShow">显示Loading</button>
</template>
<script setup>
import { inject } from 'vue'
// 方式1:通过inject获取
const loading = inject(' 版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网


发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。