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前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。