Vue3 Suspense懒加载路由组件后Loading不消失解决方法
问题描述
在实现Login路由组件懒加载时,使用<Suspense>组件搭配default、fallback插槽实现加载状态提示,加载触发逻辑正常,但存在异常:目标异步组件加载完成后,Loading状态的内容始终不会卸载消失。
相关代码
router.js 配置代码
import { createWebHistory, createRouter } from 'vue-router' const Login = () => { return new Promise((resolve) => { setTimeout(() => { resolve( import ('./components/pages/auth/Login'), ) }, 1000) }) } const Register = () => { return new Promise((resolve) => { setTimeout(() => { resolve( import ('./components/pages/auth/Register'), ) }, 1000) }) } const routes = [{ path: '/login', name: 'login', component: Login, }, { path: '/register', name: 'register', component: Register, } ] const router = createRouter({ history: createWebHistory(), routes: routes }) export default router
根组件App.vue代码
<template> <div> <Suspense> <template #default> <RouterView /> </template> <template #fallback> <div> <h1>Loading....</h1> </div> </template> </Suspense> </div> </template> <script> export default { components: { } } </script> <style scoped> div { height: 100%; width: 100%; background-color: black; } h1 { font-size: 30px; font-weight: bold; color: white; } </style>
异常表现
组件加载完成后,加载态的黑色背景仍然残留,不符合预期。
问题原因
问题由三个核心问题共同导致:
- Suspense无法感知路由组件加载状态:直接将
<RouterView />作为Suspense默认插槽内容时,<RouterView>本身是同步组件,其内部渲染异步路由组件的加载状态不会冒泡给外层Suspense,导致Suspense始终判定默认插槽内容处于未就绪状态,因此一直保留fallback插槽的Loading内容。 - 不规范的异步组件写法:手动为
import()动态导入包裹自定义Promise的写法不符合Vue 3异步组件规范,会导致Suspense无法正确识别组件加载完成的时机。 - 样式污染放大异常表现:App.vue的scoped样式中直接对
div标签设置了全屏黑色背景样式,该样式会命中组件作用域下所有div元素,哪怕后续业务组件渲染完成,外层容器div也会被加上黑色背景,进一步造成Loading完全没卸载的视觉错觉。
修复方案
按以下步骤调整代码即可解决问题:
- 调整RouterView写法,使用RouterView的作用域插槽拿到当前渲染的路由组件,直接将动态组件渲染在插槽内,保证异步组件的状态可以冒泡到外层Suspense。
- 使用官方
defineAsyncComponentAPI定义懒加载路由组件,开启suspensible: true配置支持Suspense接管加载状态,不要手动裸写Promise包裹动态导入。 - 给Loading容器添加专属类名,避免无差别标签选择器污染其他元素样式。
修复后的参考代码
router.js 修正后代码
import { createWebHistory, createRouter } from 'vue-router' import { defineAsyncComponent } from 'vue' // 用官方API定义支持Suspense的异步组件,模拟1s加载延迟 const Login = defineAsyncComponent({ suspensible: true, loader: () => new Promise((resolve) => { setTimeout(() => { resolve(import('./components/pages/auth/Login.vue')) }, 1000) }) }) const Register = defineAsyncComponent({ suspensible: true, loader: () => new Promise((resolve) => { setTimeout(() => { resolve(import('./components/pages/auth/Register.vue')) }, 1000) }) }) const routes = [ { path: '/login', name: 'login', component: Login, }, { path: '/register', name: 'register', component: Register, } ] const router = createRouter({ history: createWebHistory(), routes: routes }) export default router
App.vue 修正后代码
<template> <div class="app-container"> <Suspense> <template #default> <!-- 使用作用域插槽透传路由组件,让Suspense感知异步加载状态 --> <RouterView v-slot="{ Component }"> <component :is="Component" /> </RouterView> </template> <template #fallback> <!-- 给Loading容器加专属类名避免样式污染 --> <div class="loading-wrap"> <h1>Loading....</h1> </div> </template> </Suspense> </div> </template> <script> export default {} </script> <style scoped> .app-container { height: 100%; width: 100%; } .loading-wrap { height: 100%; width: 100%; background-color: black; } h1 { font-size: 30px; font-weight: bold; color: white; } </style>
注:如果不需要手动模拟加载延迟,路由组件可以直接简写为
() => import('./xxx/xxx.vue'),Vue Router和Vue会自动处理异步组件的Suspense适配,无需额外包裹defineAsyncComponent。
内容的提问来源于stack exchange,提问作者Joseph Onipede
相关产品推荐
相关产品推荐

