一、基础版,用全局导航守卫控制loading状态
做Vue项目时,页面切换那一下要是没loading反馈,用户很容易觉得“卡了”,那vue - router咋实现loading效果?从简单的全局加载,到和异步组件、第三方库结合,再到处理各种边界情况,这篇把思路和实操细节拆明白,新手也能跟着做~
先想最基础的逻辑:路由开始切换时显示loading,切换完成后隐藏,vue - router的全局导航守卫(beforeEach、afterEach)就是关键工具。
举个例子,我们可以在Vue实例里维护一个全局的loading状态(用vuex或者provide/inject都行),步骤如下:
-
初始化loading状态
比如在vuex里建个模块,state里有个isLoading,mutations写SET_LOADING,代码大概长这样:// store/loading.js export default { state: { isLoading: false }, mutations: { SET_LOADING(state, status) { state.isLoading = status } } }
-
导航守卫里控制状态
在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能捕获异步组件的加载状态,配合路由的话,可以这么做:
- 把路由组件改成异步组件
在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”结合起来。
做法:
- 给路由加meta标记,比如meta: { waitApi: true }
- 在路由组件的mounted里,收集所有接口请求的Promise,用Promise.all等待完成后,再通知loading隐藏。
- 全局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前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。