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

前言

什么是指令?

类似于v-modelv-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⭐ ​

相关推荐
哒哒哒5285203 分钟前
HTTP缓存
前端·面试
T___5 分钟前
从入门到放弃?带你重新认识 Headless UI
前端·设计模式
wordbaby7 分钟前
React Router 中调用 Actions 的三种方式详解
前端·react.js
黄丽萍13 分钟前
前端Vue3项目代码开发规范
前端
葬送的代码人生15 分钟前
AI Coding→像素飞机大冒险:一个让你又爱又恨的小游戏
javascript·设计模式·ai编程
curdcv_po16 分钟前
🏄公司报销,培养我成一名 WebGL 工程师⛵️
前端
Jolyne_27 分钟前
前端常用的树处理方法总结
前端·算法·面试
wordbaby29 分钟前
后端的力量,前端的体验:React Router Server Action 的魔力
前端·react.js
Alang30 分钟前
Mac Mini M4 16G 内存本地大模型性能横评:9 款模型实测对比
前端·llm·aigc
林太白30 分钟前
Rust-连接数据库
前端·后端·rust