模仿elemtnUI实现v-loading,浅浅的了解vue指令编程

2,066 阅读4分钟

废话部分

文章是csdn之前写过的。打算重新写一下,蹭蹭热度。因为之前批量迁移的时候这篇文档热度不行,排版也有问题。还有就是因为批量迁移博客,导致一些好文章没有出来。

我认为我主页里面的富文本开发部分还是挺有深度的,不过这块写的人也少,我就不多去推广了。

还有就是再回顾一下,很久没写了。只是公司项目单纯的业务流程的东西,很难让我有什么进步了,现在很多东西都靠自己去写别的来提升自己。

1、了解vue指令开发

这块我不多说,大家自己去看vue官网,还是比较详细的,主要是看这个图

让大家了解一下,指令在开发的过程中需要注意的发生的函数行为。


2、html代码部分以及参数

这块没太多可以讲的,无非就是写一个固定的蒙版而已。其实这里面很多参数性的东西意义主要在于美化和自定义

<template>
  <div
    :class="fullscreen ? 'dhtMask-FullScreen' : 'dhtMask'"
    :style="{
      zIndex: this.$dhtUI.zIndex,
      background: 'rgba(' + background + ')',
      fontSize: fontSize + 'px',
      color: color
    }"
  >
    <img
      class="dht-loading-icon"
      :src="iconSrc"
      alt="loading"
      :style="{ width: iconWidth + 'px', height: iconHeight + 'px' }"
    />
    <span>{{ text }}</span>
  </div>
</template>

<script>
export default {
  name: "dhtMask",
  data() {
    return {
      fullscreen: false,
      background: "0, 0, 0, 0.5",
      text: "加载中…",
      iconSrc: null,
      iconWidth: null,
      iconHeight: null,
      color: null,
      fontSize: null
    };
  },
  beforeCreate() {},
  created() {},
  beforeMount() {},
  mounted() {
    if (this.fullscreen) {
      document.body.style.overflow = "hidden";
    }
  },
  beforeUpdate() {},
  updated() {},
  activated() {},
  deactivated() {},
  beforeDestroy() {},
  destroyed() {
    document.body.style.overflowX = "hidden";
  },
  errorCaptured() {},
  methods: {}
};
</script>

<style lang="scss">
.dhtMask {
  position: absolute;
  z-index: 2000;
  background: rgba(0, 0, 0, 0.5);
  width: 100%;
  height: 100%;
  color: #ffffff;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 12px;
  flex-flow: column;
}
.dhtMask-FullScreen {
  @extend .dhtMask;
  width: 100vw;
  height: 100vh;
  top: 0;
  background: (0, 0, 0, 0.5);
}
.dht-loading-icon {
  width: 25px;
  height: 25px;
  object-fit: cover;
  margin-bottom: 5px;
  animation: dht-rotate 2s linear infinite;
  @keyframes dht-rotate {
    0% {
      transform: rotate(0deg);
    }
    100% {
      transform: rotate(360deg);
    }
  }
}
</style>

2、指定的函数编写(重点)

1、头部引入文件

这里有一个vue.extend,这个是关键,因为在实例化你的html代码之前,需要靠这个来把你的vue文件进行处理。这个标准格式,没太多可以说的。

import vue from "vue";
import maskLoading from "./mask.vue";

const Mask = vue.extend(maskLoading);

2、先把你的指定基本格式写好,这就是基本的格式了

//主函数
const directive = () => {
  vue.directive("dhtLoading", {
    //只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
    bind(el, binding) {
   
    },
    // 当被绑定的元素插入到 DOM 中时……
    //被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
    inserted: function(el, binding, vnode, oldVnode) {
      //console.log("元素插入的时候");
    },
    //所在组件的 VNode 更新时调用
    update(el, binding) {
     
    },
    //指令所在组件的 VNode 及其子 VNode 全部更新后调用
    componentUpdated() {
      //console.log("渲染完成了");
    },
    //只调用一次,指令与元素解绑时调用
    unbind(el) {
    
    }
  });
};

export default directive;

3、bind函数部分

这是指定函数的开始,所有初始化都在这里完成。我去掉修饰性的代码只留下核心部分

const mask = new Mask({
  el: document.createElement("div"),
  data: {
  }
});
el.instance = mask; //将mask存入
el.mask = mask.$el; //dom存入,方便获取
toggleLoading(el, binding);

这里其实是将vue文件进行实例化,初始化操作。也就是形成虚拟dom,这时候还没有插入页面。

因为我是参考elementUI的源码进行编写。讲解一下关键点。避免大家不理解

el.instance = mask; //将mask存入

这段是把实例化的vue文件暂存,后面需要额外使用

el.mask = mask.$el; //dom存入,方便获取

这里是把dom处理下

toggleLoading(el, binding);

这里el和binding,其实是指令在绑定页面的时候会传入当前绑定dom元素,还有指令参数。方式和elementUI不一样,elementUI是用的的html自定义元素。

核心在于这个函数了。很关键

4、将元素移除和插入页面toggleLoading

//蒙版操作更新
const toggleLoading = function(el, binding) {
  //console.log(binding);
  if (binding.value) {
    vue.nextTick(() => {
      if (binding.modifiers.fullscreen) {
        //全屏情况下
        //el.instance.fullscreen = true;
        //document.body.style.overflow = "hidden";
        document.body.appendChild(el.mask);
      } else {
        //el.instance.fullscreen = false;
        //非全屏情况下
        let height = el.clientHeight; //当前元素高度
        let width = el.clientWidth; //当前元素宽度
        let offsetTop = el.offsetTop; //当前元素距离顶部距离
        //给蒙版赋值
        el.mask.style.top = offsetTop + "px";
        el.mask.style.height = height + "px";
        el.mask.style.width = width + "px";
        //console.log(offsetTop);
        el.appendChild(el.mask);
      }
    });
  } else {
    //移除节点
    el.mask && el.mask.parentNode && el.mask.parentNode.removeChild(el.mask);
    el.instance && el.instance.$destroy();
  }

关键点分析:vue.nextTick

需要注意,如果不加会导致页面直接在插入的时候插入位置错误。因为是需要等待页面渲染完成的。这里其实我做的不太好,因为elementUI不会发生刷新页面导致元素定位错误的问题。不过我不多研究了,大致原因是知道的。主要是知道核心的原理。

这段代码其实很简单,一个是更新元素的宽高,可以覆盖当前的元素。还有就是定位到当前元素上面,剩下的就是直接把元素插入到页面里面。

重复调用的时候进行处理,支持删除和插入。

5、要知道指令发生变化了

//所在组件的 VNode 更新时调用
update(el, binding) {
  //console.log("更新了", binding);
  if (binding.oldValue !== binding.value) {
    toggleLoading(el, binding);
  }
},

这里很简单,就是判断一下数据是否一致,还有就是再调用一次函数,把元素移除。判断是为了防止同一个地方插入两次元素。

6、解绑指令。

//只调用一次,指令与元素解绑时调用
unbind(el) {
  //console.log("解绑了");
  //不知道指令如何解绑,先写着
  el.mask && el.mask.parentNode && el.mask.parentNode.removeChild(el.mask);
  el.instance && el.instance.$destroy();
}

这里不是很清楚。因为不知道解绑指令在什么时候。

7、整个指令的js文件

先讲注册使用。这个很简单。

就是,引入如你的js文件

import directive from "./mask/directive";
vue.use(directive);

整个js文件部分

import vue from "vue";
import maskLoading from "./mask.vue";

const Mask = vue.extend(maskLoading);

//主函数
const directive = () => {
  //蒙版操作更新
  const toggleLoading = function(el, binding) {
    //console.log(binding);
    if (binding.value) {
      vue.nextTick(() => {
        if (binding.modifiers.fullscreen) {
          //全屏情况下
          //el.instance.fullscreen = true;
          //document.body.style.overflow = "hidden";
          document.body.appendChild(el.mask);
        } else {
          //el.instance.fullscreen = false;
          //非全屏情况下
          let height = el.clientHeight; //当前元素高度
          let width = el.clientWidth; //当前元素宽度
          let offsetTop = el.offsetTop; //当前元素距离顶部距离
          //给蒙版赋值
          el.mask.style.top = offsetTop + "px";
          el.mask.style.height = height + "px";
          el.mask.style.width = width + "px";
          //console.log(offsetTop);
          el.appendChild(el.mask);
        }
      });
    } else {
      //移除节点
      el.mask && el.mask.parentNode && el.mask.parentNode.removeChild(el.mask);
      el.instance && el.instance.$destroy();
    }
  };
  //let timer = "";
  vue.directive("dhtLoading", {
    //只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
    bind(el, binding) {
      //console.log(el, binding, vnode);
      let background = binding.value.background,
        text = binding.value.text,
        iconSrc = binding.value.iconSrc,
        iconWidth = binding.value.iconWidth,
        iconHeight = binding.value.iconHeight,
        color = binding.value.color,
        fontSize = binding.value.fontSize;
      const mask = new Mask({
        el: document.createElement("div"),
        data: {
          fullscreen: !!binding.modifiers.fullscreen,
          background: background ? background : "0, 0, 0, 0.5",
          text: text ? text : "加载中…",
          iconSrc: iconSrc ? iconSrc : require("../../style/img/loading.png"),
          iconWidth: iconWidth ? iconWidth : null,
          iconHeight: iconHeight ? iconHeight : null,
          color: color ? color : null,
          fontSize: fontSize ? fontSize : null
        }
      });
      el.instance = mask; //将mask存入
      el.mask = mask.$el; //dom存入,方便获取
      toggleLoading(el, binding);
    },
    // 当被绑定的元素插入到 DOM 中时……
    //被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
    // eslint-disable-next-line no-unused-vars
    inserted: function(el, binding, vnode, oldVnode) {
      //console.log("元素插入的时候");
    },
    //所在组件的 VNode 更新时调用
    update(el, binding) {
      //console.log("更新了", binding);
      if (binding.oldValue !== binding.value) {
        toggleLoading(el, binding);
      }
    },
    //指令所在组件的 VNode 及其子 VNode 全部更新后调用
    componentUpdated() {
      //console.log("渲染完成了");
    },
    //只调用一次,指令与元素解绑时调用
    unbind(el) {
      //console.log("解绑了");
      //不知道指令如何解绑,先写着
      el.mask && el.mask.parentNode && el.mask.parentNode.removeChild(el.mask);
      el.instance && el.instance.$destroy();
    }
  });
};

export default directive;

8、文档

指令模式(蒙版)fullscreenBooleanv-dht-loading.fullscreen在非全屏模式下,dom没有渲染完成下会导致元素顶部对齐
backgroundString0, 0, 0, 0.5
textString加载中...
iconSrcString默认加载图修改加载提示图片
iconWidthString图片宽度
iconHeightString图片高度
colorString字体颜色
fontSizeString字体大小

3、整体思路和致谢

思路:

指定模式并没有很特别,更多是是一种组件的更优雅的使用方式。

像这个整体就是先绑定到当前元素,然后获取当前元素的宽高,设置蒙版宽高一致,并且定位到当前元素位置,最后插入。必要的时候就是检测数据变化,然后移除元素。本身并没有很神奇,但是大家没写之前真的会觉得,这真是太棒了。

对了说一下我的git:避免大家懵逼。看到execl文件没有,可以直接去看。

https://github.com/ht-sauce/dream


再次致谢elementUI开源组件