基于Naive UI封装的loading自定义指令

24 阅读3分钟

Naive UI 是一个 Vue3 的组件库。官网: Naive UI

基于Naive UI封装一个自定义指令

UI渲染

<template>
  <div v-if="state.show" class="loading-box" :style="state.loadingBoxStyle">
    <div class="mask" :style="{ background: state.maskBackground }"></div>
    <div class="loading-content-box">
      <n-spin v-if="state.show" :size="state.size" :rotate="state.rotate" :stroke="state.stroke">
        <template #description>
          <div v-if="state.description !== false" :style="{ color: state.textColor }" class="tip">
            {{ state.description }}
          </div>
        </template>
      </n-spin>
    </div>
  </div>
</template>

<script setup lang="ts">
import { NSpin } from 'naive-ui'

const state = reactive({
  show: false,
  description: '加载中...' as any,
  rotate: true,
  size: 'medium' as any,
  stroke: '#0052d9',
  maskBackground: 'rgba(255, 255, 255, 0.5)',
  textColor: '#0052d9',
  loadingBoxStyle: {},
})

type ILoadingInfo = {
  show?: boolean
  description?: any
  size?: string
  rotate: boolean
  maskBackground?: string
  textColor?: string
  loadingBoxStyle?: string
}

// 更新loading信息
const updateLoading = (loadingInfo: ILoadingInfo) => {
  const { show, description, maskBackground, size, rotate, textColor, loadingBoxStyle, stroke } =
    loadingInfo as any
  state.show = show || state.show
  // false: 不显示  true、不传:显示默认
  state.description =
    description === false
      ? false
      : description === true || !!description === false
      ? state.description
      : description

  state.size = size || state.size
  state.rotate = rotate || state.rotate
  state.stroke = stroke || state.stroke
  state.textColor = textColor || state.textColor
  state.maskBackground = maskBackground || state.maskBackground
  state.loadingBoxStyle = loadingBoxStyle || state.loadingBoxStyle
}

defineExpose({
  updateLoading,
})
</script>
<style lang="less" scoped>
.loading-box {
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
  bottom: 0;
  width: 100%;
  height: 100%;
  overflow: hidden;
  z-index: 99;
  .n-spin {
    color: #ccc;
  }
  .mask {
    width: 100%;
    height: 100%;
  }
  .loading-content-box {
    position: absolute;
    left: 0;
    top: 0;
    right: 0;
    bottom: 0;
    width: 100%;
    height: 100%;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
  }
  .tip {
    font-size: 14px;
    margin-top: 8px;
  }
}
</style>

相关逻辑

import { createApp } from 'vue'
import Loading from './Loading.vue'

const addClass = (el: HTMLElement, className: string) => {
  // 如果当前元素样式列表中没有className
  if (!el.classList.contains(className)) {
    el.classList.add(className)
  }
}

const removeClass = (el: HTMLElement, className: string) => {
  el.classList.remove(className)
}

// 元素挂载的操作
const append = (el: HTMLElement | any) => {
  // 根据loading组件样式,是使用absolute,而当el不是fixed或relative时候给其动态添加定位属性
  const style = getComputedStyle(el)
  // 判断el的样式中有无定位,===-1就是没有 希望v-loading不受样式限制
  if (['absolute', 'fixed', 'relative'].indexOf(style.position) === -1) {
    addClass(el, relativeCls)
  }

  // 因为loading组件生成的实例instance已经赋值给el.instance属性上了,所以在这里可以直接通过el拿到
  // el.instance.$el就是loading组件的DOM对象
  el.appendChild(el?.instance?.$el)
}

const remove = (el: HTMLElement | any) => {
  removeClass(el, relativeCls)
  el.removeChild(el?.instance?.$el)
}

let relativeCls = 'g-loading'

const vLoading = {
  // 主要写一些钩子函数 在钩子中去实现逻辑
  // 指令主要是将loading组件生成的DOM动态插入到指令作用的DOM对象上(v-loading=true),如果v-loading=false那么就删除动态插入的
  // 指令挂载时的钩子函数
  mounted(el: HTMLElement | any, binding: any) {
    const { gLoading = false } = binding.value || {}
    // el指向指令所在的dom 如 <div v-loading="true" id="box"> 那么el就是#box binding.value就是代表的true
    // 判断v-loading值为true动态插入到指令作用的节点下
    // 如果创建组件对应的dom?先用这个loading组件新建一个vue实例(app对象),然后再动态取挂载,就会产生一个实例,在实例中拿到它的DOM对象
    const app = createApp(Loading)
    // 拿到它的实例,挂载到动态创建的DOM上,vue开发是支持多实例的,可以创建多个实例
    // 因为创建的元素没挂载到BODY上,实际也没有完成dom层的挂载,目的是创建出来的实例的DOM对象要挂载到el上(指令所在的DOM)
    const instance = app.mount(document.createElement('div'))
    // 因为instance在mounted中只创建一次,但是之后会经常用到,要保留起来,如果要在其他的钩子函数也要访问它的话就存在el对象上
    // 这样操作在其他钩子中也可以获取到这个实例
    el.instance = instance

    // 局部、全局
    relativeCls = gLoading ? 'g-loading' : 'b-loading'

    // binding.value就是代表指令传递的值
    if (binding.value.show) {
      // 执行实例中的方法
      el.instance.updateLoading(binding.value)
      append(el)
    }
  },
  // 当组件更新的时候执行,因为指令不是一成不变的比如由v-loading=true变为v-loading=false 就会执行
  updated(el: HTMLElement | any, binding: any) {
    const { gLoading = false } = binding.value || {}

    // 执行实例中的方法
    el?.instance?.updateLoading(binding.value)

    // 局部、全局
    relativeCls = gLoading ? 'g-loading' : 'b-loading'

    // 如果loading前后值不一致
    if (binding.value.show !== binding.oldValue.show) {
      // 如果是true那么就插入否则删除
      binding.value.show ? append(el) : remove(el)
    }
  },
}

// 如果要在全局中使用,就在main.js中引入并注册
export default vLoading