Code前端首页关于Code前端联系我们

p>做项目时,加载状态没处理好特影响体验,比如页面切换、数据请求时用户盯着空白页干等。Vue3 里咋搞全局 Loading 呢?这篇从基础用法到进阶优化,把思路和实操步骤拆明白,新手也能跟着做

terry 2周前 (10-03) 阅读数 62 #Vue

全局 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 实现炫酷的加载动效(比如品牌化的动画),步骤:

  1. 用 AE 做动画,导出为 JSON 文件。
  2. 安装 lottie-webnpm i lottie-web
  3. 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前端网发表,如需转载,请注明页面地址。

发表评论:

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

热门