前言
其实大部分UI框架都有自己的Loading组件,有些朋友可能会说:“重复造轮子,没有意义”。
但在某些定制化需求或者想要自由度更高一点的情况下,UI框架的效果是无法满足的。同时我觉得对于很少封装组件的开发来说,学习一下这种思路也是没有什么坏处的,因为我就是这种开发。
组件代码量不多,这篇文章来深入的剖析一个Vue3全局加载组件,从设计思路到具体实现,一步步构建一个可配置、可复用、带动画、支持文本动态更新的Loading组件,并且细说在路由、Axios中的使用,让你的项目体验瞬间提升一个档次!
效果预览:

今天要做的这个组件,具备以下功能:
• 支持全屏遮罩 • 可自定义背景色、文字、Spinner 类型 • 支持锁屏(禁止滚动) • 支持动态修改 Loading 文字 • 带平滑动画过渡 • 支持函数式调用( $loading.show())
项目结构一览
我们这个组件主要由三个文件构成:
/components/GlobalLoading/ ├── GlobalLoading.vue # 核心 UI 组件 ├── loading.ts # 服务逻辑 & 函数式调用 └── index.ts # 统一导出入口
一、搭建 Loading 的 UI 界面
1. GlobalLoading.vue 基础结构
<template>
<Transition name="fade">
<div v-if="visible" class="global-loading" :class="{ 'global-loading--fullscreen': fullscreen }">
<!-- 遮罩层 -->
<div class="loading-mask" :style="maskStyle"></div>
<!-- 内容区 -->
<div class="loading-content">
<div class="loading-spinner">
<!-- 不同类型的加载动画 -->
</div>
<div v-if="internalText" class="loading-text">{{ internalText }}</div>
</div>
</div>
</Transition>
</template>说明:
• 使用 Transition实现淡入淡出动画• v-if="visible"控制显示隐藏• loading-mask是半透明遮罩• loading-content居中显示加载图标和文字
2. 支持多种加载动画(Spinner)
我们支持三种样式:圆形旋转、三点跳动、默认圆圈。
<div class="loading-spinner"> <!-- 圆形旋转 --> <div v-if="spinnerType === 'circle'" class="spinner-circle"></div> <!-- 三点跳动 --> <div v-else-if="spinnerType === 'dots'" class="spinner-dots"> <div class="dot"></div> <div class="dot"></div> <div class="dot"></div> </div> <!-- 默认 --> <div v-else class="spinner-default"> <div class="spinner"></div> </div> </div>
每种动画都通过 CSS @keyframes 实现,比如旋转动画:
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}3. Props 配置项详解
通过defineProps定义丰富的配置项:
const props = defineProps({
visible: { // 是否显示
type: Boolean,
default: false
},
text: { // 显示文字
type: String,
default: ''
},
background: { // 遮罩层颜色
type: String,
default: ''
},
spinnerType: { // 动画类型(circle/dots/default)
type: String,
default: 'circle',
validator: (value) => ['circle', 'dots', 'default'].includes(value)
},
fullscreen: { // 是否全屏
type: Boolean,
default: true
},
lock: { // 是否锁定页面滚动
type: Boolean,
default: true
},
customClass: { // 自定义类名
type: String,
default: ''
}
})validator还对spinnerType做了合法性校验,避免传错值。
4. 动态更新文字
一个关键点:如何在显示后动态修改 Loading 文字?
我们使用 defineExpose 暴露一个 setText 方法:
const internalText = ref(props.text)
watch(() => props.text, (newText) => {
internalText.value = newText
})
defineExpose({
setText: (text) => {
internalText.value = text
}
})这样外部就可以调用loadingInstance.setText('正在上传...')来实时更新文字!
二、实现函数式调用(loading.ts)
我们要实现像 Element Plus 那样的 $loading.show() 调用方式。
1. 核心思路
• 使用 h()创建 VNode• 使用 render()动态渲染到页面• 用一个 LoadingService管理实例和计数
import { h, render } from 'vue'
import GlobalLoading from './GlobalLoading.vue'
class LoadingService {
private instance: any = null
private count = 0
private container: HTMLElement | null = null
}count是关键:支持多次调用show(),只有最后一次hide()才真正关闭,避免关早了的问题。
2. show()方法:创建并显示Loading
show(options: string | LoadingOptions = {}): LoadingInstance {
this.count++
if (this.count === 1) {
const config = typeof options === 'string' ? { text: options } : options
// 创建 VNode
const vnode = h(GlobalLoading, {
...config,
visible: true,
})
// 渲染到 body
render(vnode, this.container)
document.body.appendChild(this.container)
// 保存暴露的方法(如 setText)
this.instance = vnode.component?.exposed
}
return {
close: () => this.hide()
}
}支持传字符串:show('加载中...')
支持传对象:show({ text: '请稍候', spinnerType: 'dots' })
3. hide()方法:安全关闭
hide() {
if (this.count <= 0) return
this.count--
if (this.count === 0) {
setTimeout(() => {
render(null, this.container!)
this.container?.remove()
this.container = null
this.instance = null
this.toggleBodyLock(false) // 解锁滚动
}, 300) // 等待动画结束
}
}使用setTimeout是为了让淡出动画播放完再移除DOM。
4. 锁屏功能:禁止页面滚动
我们通过给body添加loading-lock类来禁止滚动:
.loading-lock {
overflow: hidden !important;
}private toggleBodyLock(lock: boolean) {
if (lock) {
document.body.classList.add('loading-lock')
} else {
document.body.classList.remove('loading-lock')
}
}用户在Loading时无法滚动页面,体验更佳。
三、封装插件,全局可用(index.ts)
为了让组件更容易使用,我们导出为一个 Vue 插件:
// index.ts
import GlobalLoading from './GlobalLoading.vue'
import loadingService, { LoadingPlugin, useLoading } from './loading'
export {
GlobalLoading,
LoadingPlugin,
useLoading
}
export default loadingService并在main.ts中安装:
import { createApp } from 'vue'
import App from './App.vue'
import LoadingPlugin from '@/components/GlobalLoading'
const app = createApp(App)
app.use(LoadingPlugin) // 安装插件
app.mount('#app')安装后,你可以在任何组件中这样使用:
// 方式1:通过 this
this.$loading.show('加载中...')
// 方式2:通过 useLoading()
import { useLoading } from '@/components/GlobalLoading'
const loading = useLoading().show('正在提交...')
loading.close()实际使用示例
1. 在组合式 API 中使用(推荐)
<!-- 任何 .vue 文件 -->
<template>
<div class="demo">
<button @click="showBasicLoading">基本 Loading</button>
<button @click="showTextLoading">带文字 Loading</button>
<button @click="showCustomLoading">自定义 Loading</button>
</div>
</template>
<script setup lang="ts">
import { useLoading } from '@/components/GlobalLoading'
// 获取 loading 实例
const loading = useLoading()
// 基本用法
const showBasicLoading = () => {
const instance = loading.show()
// 3秒后自动关闭
setTimeout(() => {
instance.close()
}, 3000)
}
// 带文字提示
const showTextLoading = () => {
const instance = loading.show('正在加载中...')
setTimeout(() => {
instance.close()
}, 2000)
}
// 自定义配置
const showCustomLoading = () => {
const instance = loading.show({
text: '自定义加载效果',
background: 'rgba(0, 0, 0, 0.7)',
spinnerType: 'dots',
lock: true
})
setTimeout(() => {
instance.close()
}, 2500)
}
</script>2. 在选项式 API 中使用
<template>
<div>
<button @click="showLoading">选项式 API 使用</button>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'OptionsApiDemo',
methods: {
showLoading() {
// 通过 this.$loading 访问
const instance = (this as any).$loading.show('选项式 API 示例')
setTimeout(() => {
instance.close()
}, 2000)
},
async fetchData() {
try {
(this as any).$loading.show('加载数据中...')
// 模拟 API 调用
await new Promise(resolve => setTimeout(resolve, 1500))
console.log('数据加载成功')
} finally {
(this as any).$loading.hide()
}
}
},
mounted() {
// 在生命周期中使用
this.fetchData()
}
})
</script>3. 在 js/ts 文件中使用
// utils/api.ts
import loadingService from '@/components/GlobalLoading'
export class ApiService {
static async request(url: string, options?: RequestInit) {
// 显示 loading
loadingService.show('请求中...')
try {
const response = await fetch(url, options)
const data = await response.json()
return data
} catch (error) {
console.error('请求失败:', error)
throw error
} finally {
// 隐藏 loading
loadingService.hide()
}
}
}
// 在普通 TS/JS 文件中使用
export function someUtilityFunction() {
const loading = loadingService.show('处理中...')
// 执行一些操作
setTimeout(() => {
loading.close()
}, 1000)
}4. 在 Vuex/Pinia 中使用
// stores/userStore.ts
import { defineStore } from 'pinia'
import loadingService from '@/components/GlobalLoading'
export const useUserStore = defineStore('user', {
actions: {
async fetchUsers() {
loadingService.show('加载用户列表...')
try {
// 模拟 API 调用
await new Promise(resolve => setTimeout(resolve, 2000))
return [{ id: 1, name: '用户1' }]
} finally {
loadingService.hide()
}
},
async createUser(userData: any) {
const instance = loadingService.show('创建用户中...')
try {
await new Promise(resolve => setTimeout(resolve, 1500))
console.log('用户创建成功')
} finally {
instance.close()
}
}
}
})5. 在路由守卫中使用
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import loadingService from '@/components/GlobalLoading'
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue')
},
...更多配置
]
})
// 路由守卫中使用 loading
let loadingShown = false
router.beforeEach((to, from, next) => {
// 避免重复显示
if (!loadingShown) {
loadingService.show('页面加载中...')
loadingShown = true
}
next()
})
router.afterEach(() => {
// 等待路由准备就绪后再隐藏 loading
router.isReady().then(() => {
setTimeout(() => {
if (loadingShown) {
loadingService.hide()
loadingShown = false
}
}, 100)
})
})
// 处理路由错误
router.onError((error) => {
if (loadingShown) {
loadingService.hide()
loadingShown = false
}
console.error('路由错误:', error)
})
export default router6. 在 axios 拦截器中使用
// utils/request.ts
import axios from 'axios'
import loadingService from '@/components/GlobalLoading'
// 请求计数器,避免多个请求时重复显示 loading
let requestCount = 0
const instance = axios.create({
baseURL: '/api',
timeout: 10000
})
// 请求拦截器
instance.interceptors.request.use(config => {
// 如果是 GET 请求或需要显示 loading 的请求
if (config.method?.toUpperCase() === 'GET' || config.showLoading !== false) {
requestCount++
if (requestCount === 1) {
loadingService.show(config.loadingText || '加载中...')
}
}
return config
})
// 响应拦截器
instance.interceptors.response.use(
response => {
if (response.config.method?.toUpperCase() === 'GET' || response.config.showLoading !== false) {
requestCount--
if (requestCount === 0) {
loadingService.hide()
}
}
return response
},
error => {
if (error.config?.method?.toUpperCase() === 'GET' || error.config?.showLoading !== false) {
requestCount--
if (requestCount === 0) {
loadingService.hide()
}
}
return Promise.reject(error)
}
)
export default instance总结
函数式调用 h() + render()防重复关闭 count动态更新文本 defineExpose锁屏滚动 body.loading-lock { overflow: hidden }多种动画 @keyframes + 条件渲染平滑过渡 <Transition>类型安全










