Vue3 全局loading指令
Eddie 人气:1前言
- 公司在开发组件库的时候,我在封装
loading
这个组件时,需要⼀个全局使⽤的loading
,⽤插件也可以搞定,但是项⽬开发周期不太紧张,所以决定⾃⼰手写⼀个。 - 写⼀个全局的
componnets
也⾏,但是需要在每个使⽤的页⾯进⾏导⼊并且注册,因为loading
⽤到的地⽅会很多,所以我觉得需要用更优雅的方式来实现它。
1.完成loading组件
<template> <div class="mask"> <div class="loader"> <div> <div class="dot"></div> <div class="dot"></div> <div class="dot"></div> <div class="dot"></div> <div class="dot"></div> </div> <div class="tip-text">{{ title }}</div> </div> </div> </template> <script setup> import { ref, defineExpose } from 'vue' let title = ref('') // 加载提示文字 // 更改加载提示文字 const setTitle = (val) => { title.value = val } // 暴露出去 defineExpose({ title, setTitle }) } </script> <style lang="less" scoped> .mask { width: 100%; height: 100%; position: absolute; top: 0; left: 0; background-color: rgba(0, 0, 0, 0.8); z-index: 99; .loader { position: absolute; top: 50%; left: 40%; margin-left: 10%; transform: translate3d(-50%, -50%, 0); display: flex; flex-direction: column; align-items: center; } .tip-text { color: #3793ff; padding-top: 10px; } .dot { width: 18px; height: 18px; border-radius: 100%; display: inline-block; animation: slide 1s infinite; } .dot:nth-child(1) { animation-delay: 0.1s; background: #1fbfff; } .dot:nth-child(2) { animation-delay: 0.2s; background: #2ea4ff; } .dot:nth-child(3) { animation-delay: 0.3s; background: #3793ff; } .dot:nth-child(4) { animation-delay: 0.4s; background: #3b89ff; } .dot:nth-child(5) { animation-delay: 0.5s; background: #4577ff; } } @-moz-keyframes slide { 0% { transform: scale(1); } 50% { opacity: 0.3; transform: scale(2); } 100% { transform: scale(1); } } @-webkit-keyframes slide { 0% { transform: scale(1); } 50% { opacity: 0.3; transform: scale(2); } 100% { transform: scale(1); } } @-o-keyframes slide { 0% { transform: scale(1); } 50% { opacity: 0.3; transform: scale(2); } 100% { transform: scale(1); } } @keyframes slide { 0% { transform: scale(1); } 50% { opacity: 0.3; transform: scale(2); } 100% { transform: scale(1); } } </style>
2.新建⼀个loading.js⽂件,⽤来写loading的⾃定义指令逻辑
const loadingDirective = { mounted(el,binding) { }, updated(el,binding) { }, } return loadingDirective // 导出
在创建的 loadingDirective
对象中,写⼊两个钩⼦函数,因为我们希望在这两个生命周期函数,对它进⾏操作。
2.1 创建这个组件对应的 DOM
- 新建⼀个新的vue实例,再对它进⾏⼀个动态的挂载,挂载之后我们就可以拿到这个
DOM
的实例了
具体实现看如下代码块
import { createApp } from 'vue' // 导⼊ createApp ⽅法 import Loading from './loading' // 导⼊我们写好的 loading 组件 const loadingDirective = { mounted(el, binding) { // 创建app对象跟组件为我们写好的 loading 组件 const app = createApp(Loading) // 动态创建⼀个div节点,将app挂载在div上 // 我们的 loading 组件将替换此 div 标签的 innerHTML const instance = app.mount(document.createElement('div')) }, updated(el, binding) {}, } return loadingDirective // 导出
2.2 在 loading.js 文件中新增两个方法,分别是插入节点和移除节点。
import { createApp } from 'vue' // 导⼊ createApp ⽅法 import Loading from './loading' // 导⼊我们写好的 loading 组件 const loadingDirective = { mounted(el, binding) { // 创建app对象跟组件为我们写好的 loading 组件 const app = createApp(Loading) // 动态创建⼀个div节点,将app挂载在div上 // 我们的 loading 组件将替换此 div 标签的 innerHTML const instance = app.mount(document.createElement('div')) // 因为在updated也需要⽤到 instance 所以将 instance 添加在 el 上 // 在 updated中 通过el.instance 可访问到 el.instance = instance // v-loading传过来的值储存在 binding.value 中 if (binding.value) { append(el) } }, updated(el, binding) { remove(el) }, } return loadingDirective // 导出 // 插入节点 function append(el) { // 向el节点插⼊动态创建的 div 节点,内容就是我们的 loading 组件 el.appendChild(el.instance.$el) } // 移除节点 function remove(el) { // 移除动态创建的 div 节点 el.removeChild(el.instance.$el) }
2.3 完善 updated 周期函数
在⼀开始的时候将节点插⼊,在 v-loading 的值发⽣改变时候触发 updated 我们将节点移除, 但是这样写是不合适的,我们需要完善⼀下在 updated 中的操作。
updated (el, binding) { // 如果value的值有改变,那么我们去判断进⾏操作 if (binding.value !== binding.oldValue) { // 三元表达式 true 插入 false 移除 binding.value ? append(el) : remove(el) }
基本的指令已经完成的差不多了。
2.4 解决存在的三个问题
- 位置定位的问题,如果所在挂载的
DOM
元素有定位属性,那么没有问题,但是所在挂载的DOM
元素没有定位的时候,那么它⾃⾝的绝对定位,就有可能出现问题。 - 页面滚动的问题,如果出现
loading
效果, 遮罩层后面的页面会发生滚动,滚动遮罩层时会造成底部页面跟着一块滚动。 - 加载文字需求,我们还可以加入⼀段⽂字进⾏展⽰,不同的地⽅需要等待时的⽂字会有不同。
2.4.1 ⾸先来解决第⼀个和第二个问题
- 在插⼊节点的时候去判断挂载的节点有没有定位,如果没有则为其添加相对定位。
- 在插⼊节点的时候为其添加禁止/隐藏滚动。
- 在移除节点的时候移除类名。
const relative = 'g-relative' // g-relative 全局相对定位样式名称 const hidden = 'g-hidden' // g-hidden 全局禁止/隐藏滚动样式名称 // 插入节点 function append(el) { const style = getComputedStyle(el) el.classList.add(hidden) // 添加类名 if (['absolute', 'relative', 'fixed'].indexOf(style.position) === -1) { el.classList.add(relative) // 添加类名 } // 向el节点插⼊动态创建的 div 节点,内容就是我们的 loading 组件 el.appendChild(el.instance.$el) } // 移除节点 function remove(el) { // 移除动态创建的 div 节点 el.removeChild(el.instance.$el) el.classList.remove(relative) // 移除类名 el.classList.remove(hidden) // 移除类名 }
g-relative
g-hidden
是我在全局css⽂件中写的样式
.g-relative { position: relative; } .g-hidden { overflow: hidden; }
2.4.2 最后解决第三个问题,动态显示加载文字。
在⽤的地⽅使⽤
:[]
的语法
<template> <div class="demo" v-loading:[title]="loading"></div> </template> <script setup> import { ref } from 'vue' const title = ref('拼命加载中...') const loading = ref(true) } </script>
- 传⼊
title
之后需要进⾏接收,并插⼊title
在loading
中 - 通过
binding.arg
可接收到:[]
传来的值
import { createApp } from 'vue' // 导⼊ createApp ⽅法 import Loading from './loading' // 导⼊我们写好的 loading 组件 const loadingDirective = { mounted(el, binding) { // 创建app对象跟组件为我们写好的 loading 组件 const app = createApp(Loading) // 动态创建⼀个div节点,将app挂载在div上 // 我们的 loading 组件将替换此 div 标签的 innerHTML const instance = app.mount(document.createElement('div')) // 因为在updated也需要⽤到 instance 所以将 instance 添加在 el 上 // 在 updated中 通过el.instance 可访问到 el.instance = instance // v-loading传过来的值储存在 binding.value 中 if (binding.value) { append(el) } // 在此判断是否有title值 if (binding.arg !== 'undefined') { // setTitle 使我们在loading组件中定义的⽅法 el.instance.setTitle(binding.arg) } }, updated (el, binding) { // 在此判断是否有title值 if (binding.arg !== 'undefined') { // setTitle 使我们在loading组件中定义的⽅法 el.instance.setTitle(binding.arg) } // 如果value的值有改变,那么我们去判断进⾏操作 if (binding.value !== binding.oldValue) { // 三元表达式 true 插入 false 移除 binding.value ? append(el) : remove(el) } } } return loadingDirective // 导出 const relative = 'g-relative' // g-relative 全局相对定位样式名称 const hidden = 'g-hidden' // g-hidden 全局禁止/隐藏滚动样式名称 // 插入节点 function append(el) { const style = getComputedStyle(el) el.classList.add(hidden) // 添加类名 if (['absolute', 'relative', 'fixed'].indexOf(style.position) === -1) { el.classList.add(relative) // 添加类名 } // 向el节点插⼊动态创建的 div 节点,内容就是我们的 loading 组件 el.appendChild(el.instance.$el) } // 移除节点 function remove(el) { // 移除动态创建的 div 节点 el.removeChild(el.instance.$el) el.classList.remove(relative) // 移除类名 el.classList.remove(hidden) // 移除类名 }
3.在main.js文件中引⼊注册
想要在全局使⽤这个自定义指令,那么我们需要在
main.js
文件中去引⼊注册。
import { createApp } from 'vue' import App from './App.vue' import '@/assets/scss/global.css' // 引入全局样式文件 import loadingDirective from '@/views/loading/directive' //引⼊loading⾃定义指令 createApp(App) .directive('loading', loadingDirective) // 全局注册loading指令 .mount('#app')
4.在页面中使用自定义loading指令
在
onMounted
的时候,判断loading
是否显⽰,在做判断之前,先来看看我们在写好loading
⾃定义指令之后,该如何在页面中使用它。
<template> <div v-loading="loading"></div> // 只需在节点上写上 v-loading='loading' 即可,后边的loading是⼀个值 </template> <script setup> import { ref, onMounted } from 'vue' const loading = ref(true) // 默认让 loading 值为true 传给loading组件 onMounted(() => { // 定时器模拟数据加载完成之后更改 loading 状态 setTimenout(() => { loading.value = false }, 3000) }) </script>
此时我们在 loading 组件中就可以接收到传⼊的值,我们根据值来判断是否显⽰ loading 组件
加载全部内容