前言
在用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:
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 中。样式自定义。
总结
-
实现一个自定义加载指令封装,写了个小demo
-
getComputedStyle和class集合classList
-
vue指令的钩子函数和value传递
参考资料
[1] vue.js
cn.vuejs.org/v2/guide/cu…
来源:
[1] 博客 blog
mjcelaine.top