vue3 自定义全局 loading 功能
vue3 项目结构如下
frontend
┣ src
┃ ┣ assets
┃ ┣ components
┃ ┃ ┣ Loading
┃ ┃ ┃ ┣ index.ts
┃ ┃ ┃ ┗ index.vue
┃ ┃ ┗ index.ts
┃ ┣ layout
┃ ┃ ┗ index.vue
┃ ┣ pages
┃ ┃ ┣ home
┃ ┃ ┃ ┗ index.vue
┃ ┣ router
┃ ┃ ┗ index.ts
┃ ┣ App.vue
┃ ┣ global.d.ts
┃ ┣ main.ts
┃ ┣ shims.d.ts
┃ ┗ vite-env.d.ts
┣ types
┃ ┣ auto-imports.d.ts
┃ ┗ components.d.ts
┣ .dockerignore
┣ .editorconfig
┣ .eslintrc
┣ .gitignore
┣ .npmrc
┣ README.md
┣ commitlint.config.js
┣ commitlint.config.ts
┣ index.html
┣ package.json
┣ pnpm-lock.yaml
┣ tsconfig.json
┣ tsconfig.node.json
┣ unocss.config.ts
┗ vite.config.ts
创建 Loading 组件
在 components
目录下创建 Loading
文件夹,然后在 Loading
文件夹下创建 index.vue
和 index.ts
文件。
index.vue
文件内容
<script lang='ts' setup>
export interface Props {
color: string
}
withDefaults(defineProps<Props>(), {
color: '#fff',
})
const options = inject('options', {
loading: false,
text: '加载中...',
})
</script>
<template>
<div v-show="options.loading" class="loading">
<div class="loading-bar-container">
<div class="loading-bar" />
<div class="loading-bar" />
<div class="loading-bar" />
<div class="loading-bar" />
<div class="loading-bar" />
</div>
<div class="loading-text" :style="`color: ${color} !important`">
{{ options.text }}
</div>
</div>
</template>
<style scoped>
.loading {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.loading-bar-container {
display: flex;
justify-content: center;
align-items: center;
}
.loading-bar {
width: 5px;
height: 20px;
margin: 0 5px;
background-color: #5ec5cc;
animation: loading 1s ease-in-out infinite;
}
.loading-bar:nth-child(1) {
animation-delay: 0.1s;
}
.loading-bar:nth-child(2) {
animation-delay: 0.2s;
}
.loading-bar:nth-child(3) {
animation-delay: 0.3s;
}
.loading-bar:nth-child(4) {
animation-delay: 0.4s;
}
.loading-bar:nth-child(5) {
animation-delay: 0.5s;
}
.loading-text {
margin-top: 20px;
font-size: 18px;
color: #5ec5cc;
}
@keyframes loading {
0% {
transform: scale(1);
}
20% {
transform: scale(1, 2.5);
}
40% {
transform: scale(1);
}
100% {
transform: scale(1);
}
}
</style>
上述文件中定义了 Props 自定义类型,然后通过 defineProps
定义了 color
属性,最后通过 inject
注入了 options
对象,options
对象中包含了 loading
和 text
两个属性,loading
用于控制 loading 的显示和隐藏,text
用于控制 loading 的文本内容。
定义 Props 是为了在使用组件时可以自定义属性,从而控制组件的行为,这里定义了 color
属性,用于控制 loading 的颜色。
定义 inject 是为了在组件中可以使用 options
对象,从而控制 loading 的显示和隐藏,从而达到动态控制组件的效果!
index.ts
文件内容
import { createApp } from 'vue'
import type { App } from 'vue'
import LoadingComponent from './index.vue'
export type Data = Record<string, unknown>
const options = reactive({
loading: true,
text: '加载中...',
})
let loadingInstance: App
function LoadingService(rootProps: Data) {
loadingInstance = createApp(LoadingComponent, rootProps)
// provide must above mount
loadingInstance.provide('options', options)
const vm = loadingInstance.mount(document.createElement('div'))
document.body.appendChild(vm.$el)
return {
setText(text: string) {
options.text = text
},
hide() {
options.loading = false
loadingInstance.unmount()
},
}
}
export const Loading = {
install(app: App) {
app.config.globalProperties.$loading = LoadingService
},
service: LoadingService,
}
export default Loading
TIP
这里用 app.config.globalProperties.$loading
挂载了全局的属性后,如果你是 ts 开发需要扩展下 全局的属性,简单的可以在 src
文件中创建 global.d.ts
文件,然后在文件中添加如下代码:
// 正常工作。
export {}
import {Data} from './components'
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$loading: (rootProps: Data) => {
setText(text: string): void
hide(): void
}
}
}
在这个文件中定义了 options
是为了动态控制组件的显示隐藏和展示的文本内容,然后在 LoadingService
函数下通过 createApp
创建了一个新的 Vue 实例,然后通过 provide
注入了 options
对象,这样在后续更改 options
内容时组件就会相应的改变,最后通过 mount
挂载到了 body
标签下,这样就可以在任意地方使用 Loading
组件了。
注意我们在 LoadingService
方法下返回了一个对象,这个对象中包含了 setText
、hide
三个方法,这三个方法分别用于控制 loading 的显示和隐藏,以及设置 loading 的文本内容。
同时我们通过 Loading
对象下的 install
方法将 LoadingService
方法挂载到了 Vue
的原型上,这样在后续使用时就可以通过 const $loading = getCurrentInstance()!.proxy!.$loading
来调用 LoadingService
方法了。
注册 Loading 组件
在 components
目录下创建 index.ts
文件,然后在 index.ts
文件中注册 Loading
组件。
import type { App } from 'vue'
interface Plugin {
install: (app: App) => void
}
const components = import.meta.glob<Record<string, Plugin>>('./**/*.ts', { eager: true })
export function setComponents(app: App) {
Object.keys(components).forEach((key) => {
app.use(components[key].default)
})
}
export * from './Loading'
在 setComponents
方法中通过 import.meta.glob
导入了 components
目录下所有的 ts
文件,然后通过 Object.keys
遍历了所有的文件,最后通过 app.use
将所有的组件注册到了 Vue
的原型上。
注意在这里我们导出了 Loading
组件,这样在后续使用时就可以通过 import { Loading } from '@/components'
来导入 Loading
组件了。
在 main.ts
文件中导入 setComponents
方法,然后在 createApp
方法下调用 setComponents
方法,这样就可以在任意地方使用 Loading
组件了。
import { createApp } from 'vue'
import App from './App.vue'
import { setComponents } from './components'
async function bootstrap() {
const app = createApp(App)
setComponents(app)
app.mount('#app')
}
bootstrap()
使用 Loading 组件
通过挂载的全局属性 $loading
方法使用
<script lang='ts' setup>
const router = useRouter()
const $loading = getCurrentInstance()!.proxy!.$loading
$loading.show({
color: 'red',
})
setTimeout(() => {
$loading.setText('加载完成')
}, 1000)
setTimeout(() => {
$loading.hide()
}, 5000)
</script>
<template>
<div class="flex justify-center items-center max-w-lg h-screen m-auto text-4xl text-sky font-bold">
<span class="hover:text-sky-200 cursor-pointer hover:underline">Home</span>
<span class="ml-10 hover:text-sky-200 cursor-pointer hover:underline" @click="router.push('/markdown')">Markdown</span>
</div>
</template>
<style lang='scss' scoped>
</style>
通过挂载的全局 $loading
方法使用的意思是在使用 app.config.globalProperties.$loading = LoadingService
方法将 LoadingService
方法挂载到全局属性上,这样在后续使用时就可以通过 const $loading = getCurrentInstance()!.proxy!.$loading
来调用 LoadingService
方法了。
通过服务方式调用
<script lang='ts' setup>
import { Loading } from '~/components'
const router = useRouter()
const loadingInstance = Loading.service({
color: 'red',
})
setTimeout(() => {
loadingInstance.setText('正在加油加载中...')
}, 1000)
setTimeout(() => {
loadingInstance.setText('还需要加载一会儿...')
}, 1000)
setTimeout(() => {
loadingInstance.hide()
}, 5000)
</script>
<template>
<div class="flex justify-center items-center max-w-lg h-screen m-auto text-4xl text-sky font-bold">
<span class="hover:text-sky-200 cursor-pointer hover:underline">Home</span>
<span class="ml-10 hover:text-sky-200 cursor-pointer hover:underline" @click="router.push('/markdown')">Markdown</span>
</div>
</template>
<style lang='scss' scoped>
</style>
通过服务调用的方式使用 Loading
组件,这种方式即通过 import
导入 Loading
组件,然后通过 Loading.service
方法调用,这种方式可以动态控制 Loading
组件的显示和隐藏,以及设置 Loading
组件的文本内容。