使用 Vue3 自定义一个自己的 v-myLoading 指令

前言

什么是指令?

类似于v-model 或 v-show,这些是 Vue 内置的一系列指令,那除了内置指令之外,Vue 还允许我们自己注册自己所需要的指令。

今天就借助 v-loading 的效果来实现一个我们自己的 v-myLoading 吧!

效果

在线预览:Demo Show (chenyajun.fun)

原理

自定义指令的相关知识大家可以去 Vue3 官方文档看一下:

自定义指令 | Vue.js (vuejs.org)

首先我们知道我们需要实现的是 v-loading,那一个 loading 效果是什么样的呢?

可以清楚的看到,需要在目标区域垂直水平居中,并且使背景色为不完全透明的白色。

背景色好搞定,那如何使 loading 永远居中在目标容器内呢?

我们知道,如果子元素使用绝对定位,并且父元素为非 static 定位的话,便可充当参照物,就可以通过定位将子元居中在目标区域内。

那接下来我们需要做的就是:

  1. 创建 loading 组件
  2. 找到目标元素,把 loading 组件添加进去。
  3. 为目标元素设置为参照物属性,使其垂直居中显示。
  4. 对 loading 状态进行开关控制。

实现

首先创建一个用于页面显示的 loading 组件,下面核心代码,在页面中增加一个 loading 图标,并且设置无限旋转的动画即可。

js 复制代码
    .loading-icon {
      animation: rotate 1s linear infinite;
    }
    @keyframes rotate {
      from {
        transform: rotate(0deg);
      }
      to {
        transform: rotate(360deg);
      }
    }

下面是我们的文件夹结构

接下来分析一下 src\plugin\directive\loading\index.js 也就是自定义指令 v-myLoading 文件里的代码

js 复制代码
    export default {
      name: 'myLoading',
      mounted(el, binding) {
        const app = createApp(Loading)
        const instance = app.mount(document.createElement('div'))
        console.log('instance: ', instance.$el)
        const name = Loading.name
        if (!el[name]) {
          el[name] = {}
        }
        el[name].instance = instance
        if (binding.value) {
          append(el)
        }
      },
      updated(el, binding) {
        const name = Loading.name
        if (!el[name]) {
          el[name] = {}
        }
        if (binding.value !== binding.oldValue) {
          binding.value ? append(el) : remove(el)
        }
      },
    }
    function append(el) {
      const style = getComputedStyle(el)
      const name = Loading.name
      if (['absolute', 'fixed', 'relative'].indexOf(style.position) === -1) {
        if (!el.classList.contains(relativeClsss)) {
          el.classList.add(relativeClsss)
        }
      }
      el.appendChild(el[name].instance.$el)
    }
    function remove(el) {
      const name = Loading.name
      el.classList.remove(relativeClsss)
      el.removeChild(el[name].instance.$el)
    }

先分析一下 mounted 里面的代码

el: 目标元素

binding: 使用指令时所传的值 v-loading="true",此时 binding.value 为 true

instance: loading 组件实例

js 复制代码
      mounted(el, binding) {
        const app = createApp(Loading)
        const instance = app.mount(document.createElement('div'))
        console.log('instance: ', instance.$el)
        const name = Loading.name
        if (!el[name]) {
          el[name] = {}
        }
        el[name].instance = instance
        if (binding.value) {
          append(el)
        }
      },

首先创建 loading 组件的 Vue 实例 app,并挂载到临时 div 元素上,mount 方法会返回相应的组件实例赋值给 instance 变量,通过 instance.$el 拿到我们的 loading 组件元素。

js 复制代码
        const app = createApp(Loading)
        const instance = app.mount(document.createElement('div'))
        console.log('instance: ', instance.$el)

下面是打印的 instance 变量 ,可以看到已经拿到 loading 元素了;

检查目标元素上是否已经绑定了该组件实例,如果没有则初始化一个对象。

在这个对象上挂载当前的组件实例 instance,这一步是为了后面把 loading 组件添加到目标元素内。

js 复制代码
        const name = Loading.name    
       if (!el[name]) {
          el[name] = {}
        }
        el[name].instance = instance

判断loading初始状态,如果为true,就开启loading,把 loading 组件添加到目标元素内部

js 复制代码
        if (binding.value) {
          append(el)
        }

一般我们的 loading 状态初始值都为 false,在我们调用接口的时候赋值为 true,接口调用结束赋值为 false,那我们就来看一下怎么处理 loading 状态的变化,这个时候需要用到 updated 函数,他会在 loadnig 状态变化时执行。

当 loading 状态变化时,如果为 true,就把 loading 添加进去,如果为 false 就移除出去

js 复制代码
      updated(el, binding) {
        const name = Loading.name
        if (!el[name]) {
          el[name] = {}
        }
        if (binding.value !== binding.oldValue) {
          binding.value ? append(el) : remove(el)
        }
      },
    }

添加元素函数:

判断目标元素是否含有非 static 定位,如果都没有就增加一个相对定位,g-relative 为我们事先写好的全局相对定位 class。最后把 loading 组件添加到目标元素里面。

css 复制代码
.g-relative {
  position: relative;
}
js 复制代码
    const relativeClsss = 'g-relative'//为相对定位
    function append(el) {
      const style = getComputedStyle(el)
      const name = Loading.name
      if (['absolute', 'fixed', 'relative'].indexOf(style.position) === -1) {
        if (!el.classList.contains(relativeCls)) {
          el.classList.add(relativeCls)
        }
      }
      el.appendChild(el[name].instance.$el)
    }

移除元素函数:

这个就直接移除相对定位 class,移除loading组件即可

js 复制代码
    function remove(el) {
      const name = Loading.name
      el.classList.remove(relativeClsss)
      el.removeChild(el[name].instance.$el)
    }

怎么使用?

在 plugin/directive/index.js,引入我们写好的自定义文件,使用循环的方式,这样方便我们其他的指令可以一起全局注册使用。

js 复制代码
    import loadingDirective from '@/plugin/directive/loading/index.js'
    import testDirective from '@/plugin/directive/test/index.js'

    const directiveList = [loadingDirective, testDirective]
    export default {
      install: (app) => {
        directiveList.forEach((component) => {
          app.directive(component.name, component)
        })
      },
    }

最后在 main.js 中使用即可

js 复制代码
    import directive from './plugin/directive/index.js'

    app.use(directive).mount('#app')

总结

在实现一个自定义指令的时候,我们首先要明白在做的东西是什么,如果使用 js 的话应该怎么实现?其内部原理还是 js,就好比一个简单的 input 自动聚焦指令,mounted: (el) => el.focus(),只需要一行代码就行了。其次就是对于自定义指令的内部一些参数、方法知道如何使用,这个需要我们做过一次,就明白了。

源码:

GitHub地址:chenyajun-create/v-myLoading (github.com)

所有源码已经同步提交 GitHub,如果觉得对你有帮助或者启发的话,可以给作者点下 star⭐ ​

相关推荐
卸任4 小时前
Vibe coding工具使用心得
前端·ai编程
brien95 小时前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance5 小时前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M5 小时前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写5 小时前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李5 小时前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易5 小时前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
清晨Coding5 小时前
删了 Cookie 还能自动登录?Google系网站登录态自动恢复的原理分析
javascript
风骏时光牛马6 小时前
前沿视界
前端
kyriewen6 小时前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员