Skip to content

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.vueindex.ts 文件。

index.vue 文件内容

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 对象中包含了 loadingtext 两个属性,loading 用于控制 loading 的显示和隐藏,text 用于控制 loading 的文本内容。

定义 Props 是为了在使用组件时可以自定义属性,从而控制组件的行为,这里定义了 color 属性,用于控制 loading 的颜色。

定义 inject 是为了在组件中可以使用 options 对象,从而控制 loading 的显示和隐藏,从而达到动态控制组件的效果!

index.ts 文件内容

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 文件,然后在文件中添加如下代码:

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 方法下返回了一个对象,这个对象中包含了 setTexthide 三个方法,这三个方法分别用于控制 loading 的显示和隐藏,以及设置 loading 的文本内容。

同时我们通过 Loading 对象下的 install 方法将 LoadingService 方法挂载到了 Vue 的原型上,这样在后续使用时就可以通过 const $loading = getCurrentInstance()!.proxy!.$loading 来调用 LoadingService 方法了。

注册 Loading 组件

components 目录下创建 index.ts 文件,然后在 index.ts 文件中注册 Loading 组件。

ts
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 组件了。

ts
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 方法使用

vue
<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 方法了。

通过服务方式调用

vue
<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 组件的文本内容。