懒人自定义加载Vue指令,哪里用就指令一把梭

1,022 阅读3分钟

前言

在用Vue开发的时候,平时处理数据加载缓慢的时候,我总是一般都是用Vue的v-if来动态渲染组件,这样给一个等候时间。例如:

<template>
  <div class="test">
    <div v-if="isShow"><loading /></div>
    <div v-else>
      <div v-for="item in data.list" :key="item.name">{{ item.name }}</div>
    </div>
  </div>
</template>

通过在请求数据的时候,让默认值为true的v-if先渲染loading,再等待数据拿到了,将isShow赋值为false。好吧,一个组件内这样用还好,那如果多个组件都这样占位加载,这得写多少次?对于懒人的我,肯定得想一个方便的办法:“自定义指令”。而且组件内的数据在父组件下通过props传进去做,便于封装。

原理

既然当前组件在请求数据时有一段未响应时间段,在这个时间段里我们将一个自定义的loading组件当做它的子节点appendChild进去,并且loading组件本身为position:absolute,上下居中的状态。那么,当数据请求完,再把这个loading从当前dom下移除,这不就ok了吗?

简而言之:

  • mounted(dom渲染完)
    • 将绝对定位的loading加进去dom节点下
  • updated(dom子节点更新后)
    • 将绝对定位的loading移除dom节点下

封装

我们先看目录结构:

  • create-loading.js 封装一个自定义加载指令模块
  • index.js 指令出口
  • loading.gif 一个动态图
  • loading.vue 一个loading组件

loading.vue:

<template>
  <div class="loading">
    <div class="loading_content">
      <img width="24" height="24" src="./loading.gif" />
      <p class="loading_desc">{{ title }}</p>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      title: "正在载入...",
    };
  },
};
</script>

<style lang="scss" scoped>
.loading {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate3d(-50%, -50%, 0);
}
</style>

这里是一些基本样式,没啥好研究,关键在这个position:absolu定位布局而已,宽度高度要让父组件的高度有个最小值,不然会往上继承高度。我这里height让他默认撑开就行。

loading.gif:

loading.gif

index.js:

import createLoading from "./create-loading";
import Loading from "./loading.vue";

const loadingDirective = createLoading(Loading);
export default loadingDirective;

在这里导入指令创建模块和loading组件,最后loadingDirective为一个带有钩子的对象导出出去。

create-loading.js:

import { createApp } from "vue";  //引入Vue的createApp
const relative = "relative"; //定义一个position:relative 的变量
export default function createLoading(Component) {
  return {
    mounted(el, binding) {
   1  const app = createApp(Component);
      const instance = app.mount(document.createElement("div"));
      const name = Component.name;
      if (!el[name]) {
        el[name] = {};
      }
   2  const title = binding.arg;
      el[name].instance = instance;
      if (typeof title !== "undefined") {
        el[name].instance.setTitle(title);
      }
      if (binding.value) {
        append(el);
      }
    },
    updated(el, binding) {
      
     
    },
  };

}

1:在mounted中,先将loading组件传进去用Component接收。用vue的createApp创建一个组件实例,再原生js方式createElement一个div元素,通过app实例挂载一下。现在instance就是一个被div包住的loading组件。当然你直接用compoennt直接赋值也可以,但是容易被全局污染。拿到name在el上[name]设置对象,目的是为了区分开来。

2:将组件实例挂载到el[name].instance上,取出binding.arg的参数和binding.value的值,默认为true调用自定义方法放到子节点下。这样防止其他指令的干扰和污染。

封装append和remove方法,来移入和移除loading组件

  function append(el) {
    const name = Component.name;
    const style = getComputedStyle(el);
    if (["absolute", "fixed", "relative"].indexOf(style.position) == -1) {
      addClassName(el, relative);
    }
    el.appendChild(el[name].instance.$el);
  }
  function remove(el) {
    const name = Component.name;
    el.removeChild(el[name].instance.$el);
    removeClassName(el, relative);
  }

在这里通过getComputedStyle方法拿到dom节点上的style,判断是否有"absolute", "fixed", "relative"之一样式,因为absolute的定位父级别取决于有无,不然会定位上去浏览器窗口或者body上。我们再定一个加position:relative样式的方法。

这里有点坑,通过app.mount挂载的组件是一个proxy代理对象,而不是真实的dom节点。所以要通过el[name].instance.$el来拿。打印el[name].instance如下:

继续:

  function addClassName(el, className) {
    if (!el.classList.contains(className)) {
      el.classList.add(className);
    }
  }

  function removeClassName(el, className) {
    el.classList.remove(className);
  }

那接下来就是当dom节点Vnode更新后,我们再移除loading。

  updated(el, binding) {
      const name = Component.name;
      //移除loading
      if (binding.value !== binding.oldValue) {
        binding.value ? append(el) : remove(el);
      }
    },

大功告成了~,还最后一步,将请求数据过程抽离出来通过props传递。

//App.vue
<test :list="list" v-loading="list.length == 0" />
--------------------------------------------
<script>
 data() {
    return {
      list: [],
    };
  },
  mounted() {
    setTimeout(() => {//模拟网络请求延迟
      this.list = [
        { name: "我是loading" },
        { name: "我是loading" },
        { name: "我是loading" },
        { name: "我是loading" },
        { name: "我是loading" },
      ];
    }, 2000);
  },
</script>

什么?这个loading组件太丑?那就换个。

在建立一个other-loading.vue,在index.js引入,传递进去createLoading 中。样式自定义。

总结

  1. 实现一个自定义加载指令封装,写了个小demo

  2. getComputedStyle和class集合classList

  3. vue指令的钩子函数和value传递

参考资料

[1] vue.js
cn.vuejs.org/v2/guide/cu…

来源:

[1] 博客 blog
mjcelaine.top