如何在Vue3中使用Bootstrap Modal实现预加载Spinner并解决调用报错
问题根因
- 调用
Spinner.showSpinner()的时机早于App.vue组件挂载完成,路由预加载逻辑执行时,DOM中还不存在#mdlSpinner元素,给Bootstrap Modal传入空值初始化触发非法调用错误 - 你编写的Bootstrap模态框结构不符合官方要求,缺少
.modal-dialog、.modal-content等必要层级,会导致模态框显示异常、无法正常关闭 - 直接在非Vue上下文操作DOM,和Vue的渲染周期不同步,很容易出现DOM不存在、状态不同步的问题
适配多场景的全局Spinner实现方案
采用Vue3响应式状态控制Spinner的显隐,完全避免直接操作DOM,同时兼容路由守卫、普通组件内的调用需求:
1. 安装Pinia(项目已安装可跳过)
npm install pinia
在main.js中注册Pinia:
import { createApp } from 'vue' import { createPinia } from 'pinia' import App from './App.vue' const app = createApp(App) const pinia = createPinia() app.use(pinia) app.mount('#app')
2. 创建全局Spinner状态Store
新建stores/spinner.js:
import { defineStore } from 'pinia' export const useSpinnerStore = defineStore('spinner', { state: () => ({ // 用计数避免多个请求嵌套调用时提前隐藏Spinner的问题 loadingCount: 0 }), getters: { visible: (state) => state.loadingCount > 0 }, actions: { show() { this.loadingCount++ }, hide() { if (this.loadingCount > 0) { this.loadingCount-- } }, forceHide() { this.loadingCount = 0 } } })
3. 改造App.vue中的Spinner结构
用Vue指令控制显隐,补全Bootstrap模态框必要结构和适配样式:
<template> <div class="home"> <!-- 全局Spinner模态框 --> <div class="modal fade show" tabindex="-1" v-show="spinnerStore.visible" style="background: rgba(255,255,255,0.7); z-index: 9999" > <div class="modal-dialog modal-dialog-centered justify-content-center" style="margin: 0"> <div class="spinner-border text-info" role="status" style="width: 3rem; height: 3rem"> <span class="visually-hidden">Loading...</span> </div> </div> </div> <div id="login-teleport" /> <Navbar /> <div class="cam-container"> <div class="sidebar"> <Sidemenu /> </div> <div class="content"> <router-view /> <div id="popup-teleport" /> </div> </div> </div> </template> <script setup> import { useSpinnerStore } from '@/stores/spinner' const spinnerStore = useSpinnerStore() </script>
4. 封装通用调用工具,兼容非Vue上下文
新建utils/spinner.js,适配路由预加载这类非setup上下文的调用场景:
import { useSpinnerStore } from '@/stores/spinner' let storeInstance = null export const Spinner = { init(pinia) { storeInstance = useSpinnerStore(pinia) }, show() { storeInstance?.show() }, hide() { storeInstance?.hide() }, forceHide() { storeInstance?.forceHide() } }
在main.js中完成初始化:
// 注册Pinia后添加以下代码 import { Spinner } from '@/utils/spinner' Spinner.init(pinia)
5. 不同场景调用方式
路由预加载场景:
async preload(to) { Spinner.show(); let data = await Manager.getFilteredList({ filter: [], order: {}, pageinfo: { pagenum: 1, pagelength: globalVars.EVENT_PER_PAGE }, }).finally(() => Spinner.hide()); to.params.list= data.list; to.params.totalCount = data.totalcount; return to; }
组件内调用场景(比如点击按钮拉取数据):
<script setup> import { useSpinnerStore } from '@/stores/spinner' const spinnerStore = useSpinnerStore() const fetchData = async () => { spinnerStore.show() try { // 执行接口请求逻辑 } finally { spinnerStore.hide() } } </script> <template> <button @click="fetchData">拉取数据</button> </template>
内容的提问来源于stack exchange,提问作者Laterneman
相关产品推荐
相关产品推荐

