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

一、基础版,用全局导航守卫控制loading状态

terry 15小时前 阅读数 14 #Vue

做Vue项目时,页面切换那一下要是没loading反馈,用户很容易觉得“卡了”,那vue - router咋实现loading效果?从简单的全局加载,到和异步组件、第三方库结合,再到处理各种边界情况,这篇把思路和实操细节拆明白,新手也能跟着做~

先想最基础的逻辑:路由开始切换时显示loading,切换完成后隐藏,vue - router的全局导航守卫(beforeEach、afterEach)就是关键工具。

举个例子,我们可以在Vue实例里维护一个全局的loading状态(用vuex或者provide/inject都行),步骤如下:

  1. 初始化loading状态
    比如在vuex里建个模块,state里有个isLoading,mutations写SET_LOADING,代码大概长这样:

    // store/loading.js
    export default {
    state: {
     isLoading: false
    },
    mutations: {
     SET_LOADING(state, status) {
       state.isLoading = status
     }
    }
    }
  2. 导航守卫里控制状态
    在router/index.js里,用beforeEach和afterEach:

    import router from './router'
    import store from './store'

router.beforeEach((to, from, next) => { store.commit('SET_LOADING', true) // 路由开始切换,显示loading next() })

router.afterEach(() => { store.commit('SET_LOADING', false) // 路由切换完成,隐藏loading })


3. 页面里用loading组件  
比如在App.vue里,根据isLoading显示加载动画:  
```vue
<template>
  <div id="app">
    <LoadingComponent v-if="isLoading" />
    <router - view />
  </div>
</template>
<script>
import { mapState } from 'vuex'
import LoadingComponent from './components/Loading.vue'
export default {
  components: { LoadingComponent },
  computed: {
    ...mapState(['isLoading'])
  }
}
</script>

但这里有个问题:如果路由切换特别快(比如组件加载瞬间完成),loading会闪一下,用户体验反而不好,所以可以加个延迟,比如用setTimeout,或者判断路由切换的时间差,不过基础版先跑通流程,后面再优化~

还要考虑路由嵌套的情况,比如父路由和子路由都有守卫,beforeEach会按顺序触发(全局→父路由→子路由),afterEach则相反(子路由→父路由→全局),所以如果直接在全局守卫里控制loading,嵌套路由切换时loading会正常显示和隐藏,不用额外处理,因为beforeEach只要有一个触发,loading就显示,afterEach全部完成后才隐藏。

结合异步组件:给代码分割加loading反馈

Vue3里用defineAsyncComponent处理异步组件,好处是代码分割(打包时拆成单独chunk),但加载时会有延迟,这时候给用户loading反馈很重要,而路由组件本身也可以是异步组件,所以要把“路由切换loading”和“异步组件加载loading”结合起来。

思路1:用Suspense组件(Vue3专属)

Suspense能捕获异步组件的加载状态,配合路由的话,可以这么做:

  1. 把路由组件改成异步组件
    在router/index.js里:
    import { defineAsyncComponent } from 'vue'

const Home = defineAsyncComponent(() => import('./views/Home.vue')) const About = defineAsyncComponent(() => import('./views/About.vue'))

const routes = [ { path: '/', component: Home }, { path: '/about', component: About } ]


2. 在App.vue里用Suspense包裹router - view  
```vue
<template>
  <Suspense>
    <template #default>
      <router - view />
    </template>
    <template #fallback>
      <LoadingComponent /> <!-- 异步组件加载时显示这个 -->
    </template>
  </Suspense>
  <!-- 全局路由切换的loading可以和这个结合 -->
  <GlobalLoading v - if="isLoading" />
</template>

这里要注意:Suspense的fallback是处理异步组件本身加载的状态,而全局导航守卫的loading是处理路由切换流程的状态,比如用户从/home跳到/about,beforeEach触发显示全局loading,同时About组件是异步的,开始加载,Suspense显示fallback,当About组件加载完成,Suspense的default渲染,然后afterEach触发隐藏全局loading,所以两个loading可以同时存在,也可以根据需求取舍。

思路2:自己管理异步组件的加载状态

如果不用Suspense,也可以在defineAsyncComponent里加onLoading和onError钩子:

const About = defineAsyncComponent({
  loader: () => import('./views/About.vue'),
  loadingComponent: LoadingComponent, // 加载时显示的组件
  errorComponent: ErrorComponent, // 加载失败时显示的组件
  delay: 200, // 延迟200ms后显示loading,避免闪一下
  timeout: 5000 // 加载超时时间
})

这种方式更灵活,能给每个异步路由组件单独配置loading,但如果有很多路由,重复配置就很麻烦,所以可以封装一个工具函数,自动给所有异步路由组件加loading配置:

function asyncRoute(componentPath, options = {}) {
  return defineAsyncComponent({
    loader: () => import(`./views/${componentPath}.vue`),
    loadingComponent: options.loadingComponent || GlobalLoading,
    errorComponent: options.errorComponent || GlobalError,
    delay: options.delay || 100,
    timeout: options.timeout || 3000
  })
}
// 使用时
const Home = asyncRoute('Home', { delay: 0 }) // 首页加载快,不需要延迟
const About = asyncRoute('About')

这样每个路由的loading可以差异化处理,同时减少重复代码~

用NProgress实现顶部进度条效果

很多项目喜欢用顶部的进度条(类似浏览器标签页的加载条),NProgress这个库就很适合,它的原理是在页面顶部加一个逐渐前进的条,路由切换时启动,完成后结束。

步骤1:安装和引入NProgress

npm install nprogress

然后在router/index.js里引入样式和方法:

import NProgress from 'nprogress'
import 'nprogress/nprogress.css' // 引入默认样式,也可以自己改

步骤2:在导航守卫里控制进度条

router.beforeEach((to, from, next) => {
  NProgress.start() // 开始进度条
  next()
})
router.afterEach(() => {
  NProgress.done() // 结束进度条
})
// 处理路由错误的情况,比如异步组件加载失败
router.onError(() => {
  NProgress.done() // 错误时也要结束进度条,避免一直转
})

这样基本就能实现顶部进度条随路由切换前进的效果,但还有优化点:

  • 自定义样式:比如改进度条颜色、高度,直接修改nprogress.css里的样式,或者自己写CSS覆盖。

  • 控制进度条速度:NProgress可以设置minimum(最小百分比)、speed(动画速度)等,

    NProgress.configure({
    minimum: 0.1, // 最小显示10%
    speed: 500, // 动画时长500ms
    trickleSpeed: 100 // 自动前进的速度
    })
  • 结合路由元信息跳过某些页面:比如登录页不需要进度条,给路由加meta: { noProgress: true },然后在beforeEach里判断:

    router.beforeEach((to, from, next) => {
    if (!to.meta.noProgress) {
      NProgress.start()
    }
    next()
    })

这样登录页切换时就不会显示进度条,细节拉满~

精细化控制:不同场景下的loading策略

实际项目里,loading不能“一刀切”,比如有的页面要等接口全加载完再隐藏loading,有的页面加载快不需要loading,这时候得结合路由元信息、接口管理来做。

场景1:路由需要等待接口完成后再隐藏loading

假设某个路由组件mounted后要发多个接口请求,这时候即使路由切换完成(afterEach触发),但接口还在加载,用户看到页面结构出来了但数据没到,体验也不好,这时候要把“路由loading”和“接口loading”结合起来。

做法:

  1. 给路由加meta标记,比如meta: { waitApi: true }
  2. 在路由组件的mounted里,收集所有接口请求的Promise,用Promise.all等待完成后,再通知loading隐藏。
  3. 全局loading状态由“路由切换中”和“接口请求中”共同决定。

举个代码例子:

<!-- About.vue -->
<template>
  <div>{{ data }}</div>
</template>
<script>
import { ref } from 'vue'
import { useStore } from 'vuex'
import { getList, getDetail } from '@/api'
export default {
  mounted() {
    const store = useStore()
    const fetchData = async () => {
      store.commit('SET_LOADING', true) // 接口开始请求,显示loading
      try {
        const [list, detail] = await Promise.all([getList(), getDetail()])
        // 处理数据...
      } finally {
        store.commit('SET_LOADING', false) // 接口全部完成,隐藏loading
      }
    }
    fetchData()
  }
}
</script>

路由的meta标记配合:

{
  path: '/about',
  component: About,
  meta: { waitApi: true }
}

然后在全局导航守卫里,判断meta.waitApi,决定是否要等接口,不过这种方式需要每个路由组件自己处理,有点麻烦,所以可以封装一个hook:

// hooks/useApiLoading.js
import { onMounted } from 'vue'
import { useStore } from 'vuex'
export function useApiLoading(apiFns) {
  const store = useStore()
  onMounted(async () => {
    store.commit('SET_LOADING', true)
    try {
      await Promise.all(apiFns.map(fn => fn()))
    } finally {
      store.commit('SET_LOADING', false)
    }
  })
}
// 在About.vue里用
import { useApiLoading } from '@/hooks/useApiLoading'
import { getList, getDetail } from '@/api'
export default {
  mounted() {
    useApiLoading([getList, getDetail])
  }
}

这样封装后,每个需要等接口的页面用这个hook,代码更简洁~

场景2:部分路由不需要loading

比如首页是服务端渲染(SSR)的,或者静态页面,切换时很快,不需要loading,这时候用路由元信息meta: { noLoading: true },然后在全局守卫里判断:

router.beforeEach((to, from, next) => {
  if (!to.meta.noLoading) {
    store.commit('SET_LOADING', true)
  }
  next()
})
router.afterEach((to) => {
  if (!to.meta.noLoading) {
    store.commit('SET_LOADING', false)
  }
})

这样首页(假设meta.noLoading: true)切换时就不会显示loading,避免不必要的反馈~

解决loading的常见“坑”

做loading时,很容易遇到一些边界情况,比如loading闪烁、嵌套路由loading失控、加载失败后loading不消失,这里逐个解决:

坑1:loading闪烁(切换太快导致)

问题:路由切换时间极短(比如组件加载很快),loading显示后立即隐藏,用户看到一闪而过,反而更困惑。

解决方法:

  • 加延迟显示:比如路由开始切换后,延迟200ms再显示loading,如果200ms内路由已经切换完成,就不显示。
  • 记录路由开始切换的时间,在afterEach时计算时间差,如果小于300ms,就不隐藏(或者延迟隐藏)。

代码示例(延迟显示):

let timer = null
router.beforeEach((to, from, next) => {
  timer = setTimeout(() => {
    store.commit('SET_LOADING', true)
  }, 200) // 延迟200ms显示
  next()
})
router.afterEach(() => {
  clearTimeout(timer) // 路由切换完成,清除定时器
  // 如果定时器没触发(说明切换时间<200ms),就不显示loading
  store.commit('SET_LOADING', false)
})

这样如果路由切换在200ms内完成,loading不会显示;超过200ms才显示,避免闪烁。

坑2:嵌套路由loading层级问题

问题:父路由和子路由都有守卫,切换子路由时,loading可能被多次触发,导致显示异常。

解决方法:用“层级计数”的方式,每次beforeEach时计数 + 1,afterEach时计数 - 1,当计数为0时才隐藏loading。

代码示例:

let loadingCount = 0
router.beforeEach((to, from, next) => {
  loadingCount++
  if (loadingCount === 1) {
    store.commit('SET_LOADING', true)
  }
  next()
})
router.afterEach(() => {
  loadingCount--
  if (loadingCount === 0) {
    store.commit('SET_LOADING', false)
  }
})

这样不管有多少层嵌套路由,只有当所有路由切换完成(计数归0),才隐藏loading,避免中间层级导致的loading异常。

坑3:异步组件加载失败,loading不消失

问题:异步路由组件加载时出错(比如网络问题、文件不存在),这时候afterEach可能没触发,导致loading一直显示。

解决方法:在router.onError里处理错误,同时隐藏loading。

代码示例:

router.onError((error) => {
  console.error('路由加载错误:', error)
  store.commit('SET_LOADING', false)
  NProgress.done() // 如果用了NProgress,也要结束
  // 可以跳转到错误页面
  router.push('/error')
})

在defineAsyncComponent的errorComponent里,也可以触发loading隐藏,双重保险~

vue - router实现loading效果,核心是利用导航守卫控制全局状态,结合异步组件、第三方库(如NProgress)做精细化反馈,还要处理各种边界情况,从基础的全局loading,到和异步组件、接口请求联动,再到解决闪烁、层级、错误等问题,每一步都要结合用户体验去优化,实际项目里,根据需求选择合适的方案(比如简单项目用全局守卫 + loading组件,复杂项目结合异步组件和接口管理),就能让路由切换的loading既实用又好看~

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

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

热门