封装v-loading指令 从此释放双手
前言
大家好, 我是旋风冲锋 - 小瑜, 又到了周六~~ 没错, 是卷王们疯狂成长的日子, 今天早上突发奇想, 想去自习室体验一下敲代码的快感, 心想着卷到下午,面对着窗口看着夕阳西下的场景, 然后可以拍个照片发朋友圈装逼.
但是想象很美好, 坐着地铁到了自习室, 发现大家伙都是在非常安静, 唯一能出声音的就是翻书或者写字的声音. 为了不影响其他人故意挑一个靠窗户的位置,接着对着电脑开始疯狂进攻. 我的键盘声很快传遍了整见屋子. 虽然别人没有说什么, 但是自己觉得好像是故意来捣乱的, 今天的键盘声音显得格外的大声. 没多久我知趣的溜溜球. 美团体验卷直接gg, 哈哈哈, 问题不大~
言归正传, 今天给大家分享的是利用 VUE3 实现v-loading的加载效果 , 先看一下实现效果吧~
这一类效果在使用组件库, 例如饿了么中出现的频率很高, 使用方法也很简单, 给对应的结构添加上
v-loading="布尔值"即可, 是不是很好奇是怎么实现的? 那么就和旋风冲锋小瑜开始冲!
实现思路
- loading肯定也是一个组件, 其中包含加载效果还有提示文字, 并且使用的时候可以去修改文字以及开启或者关闭加载动画
- 实现的周期是在 异步开始前, 开启loading, 在异步处理[数据加载]完成后 关闭loading
- 既然是在模版中通过 v-xxx来实现的, 那么肯定就是一个自定义指令, Vue提供指令, 也就是去操作DOM[组件实例]
那么按照以上的实现思路, 一步一步去完成, 首先搭设一个Demo的模版结构和样式
搭设基本模版
利用Vue3搭设demo架子, 头部tab栏, 切换路由 , main区域的显示内容
App.vue
<script setup lang="ts"></script>
<template>
<div class="container">
// Tab栏
<Tabs></Tabs>
// 一级路由出口
<router-view></router-view>
</div>
</template>
<style lang="scss">
.container {
width: 100vw;
height: 100vh;
background-color: #1e1e1e;
}
</style>
router路由
routes: [
{
path: '/',
redirect: '/huawei'
},
{
path: '/huawei',
component: () => import('@/views/Huawei/index.vue'),
meta: {
title: '华为'
}
},
{
path: '/rongyao',
component: () => import('@/views/Rongyao/index.vue'),
meta: {
title: '荣耀'
}
},
{
path: '/xiaomi',
component: () => import('@/views/Xiaomi/index.vue'),
meta: {
title: '小米'
}
},
{
path: '/oppo',
component: () => import('@/views/Oppo/index.vue'),
meta: {
title: 'oppo'
}
}
]
Tabs组件
<script setup lang="ts">
import { ref } from 'vue'
const tabList = ref([
{ id: 1, text: '华为', path: '/huawei' },
{ id: 2, text: '荣耀', path: '/rongyao' },
{ id: 3, text: '小米', path: '/xiaomi' },
{ id: 4, text: 'oppo', path: '/oppo' }
])
const activeIndex = ref(0)
</script>
<template>
<div class="tabs-box">
<router-link
class="tab-item"
:to="item.path"
v-for="(item, index) in tabList"
:key="item.id"
>
<span
class="tab-link"
:class="{ active: activeIndex === index }"
@click="activeIndex = index"
>
{{ item.text }}
</span>
</router-link>
</div>
</template>
<style lang="scss" scoped>
.tabs-box {
width: 100%;
display: flex;
justify-content: space-around;
.tab-item {
padding: 10px;
&.router-link-active {
.tab-link {
transition: border 0.3s;
color: gold;
padding-bottom: 5px;
border-bottom: 2px solid gold;
&.active {
border-bottom: 2px solid gold;
color: gold;
}
}
}
}
}
</style>
按照路由去创建4个文件夹,这里按照huawei做举例
<script setup lang="ts">
const src = ref('')
</script>
<template>
<div class="box" >
<div class="img-box">
<img :src="src" alt="" />
</div>
</div>
</template>
创建Loading组件
首先按照最直接的方式, 利用 v-if 以及组件通讯, 实现组件方式的实现
注意 这里是通过 position: absolute; 通过定位的方式进行垂直水平居中, 先埋下伏笔
<script setup lang="ts">
defineProps({
title: {
type: String,
default: '正在加载中...'
}
})
</script>
<template>
<div class="loading-box">
<div class="loading-content">
// loading 动图
<img src="./loading.gif" />
// 底部提示文字
<p class="desc">{{ title }}</p>
</div>
</div>
</template>
<style lang="scss" scoped>
.loading-box {
position: absolute;
top: 50%;
left: 50%;
transform: translate3d(-50%, -50%, 0);
.loading-content {
text-align: center;
img {
width: 35px !important;
height: 35px !important;
}
.desc {
line-height: 20px;
font-size: 12px;
color: #fff;
position: relative;
}
}
}
</style>
在对应组件中使用Loading组件, 利用延时器模拟异步操作
<script>
const src = ref('')
const title = ref('华为加载中...')
const showLoading = ref(true) // 控制loading的显示和隐藏
onMounted(() => {
showLoading.value = true
// 模拟异步请求
window.setTimeout(() => {
src.value =
'https://ms.bdimg.com/pacific/0/pic/-1284887113_-1109246585.jpg?x=0&y=0&h=340&w=510&vh=340.00&vw=510.00&oh=340.00&ow=510.00'
showLoading.value = false
}, 1000)
})
</script>
<template>
<div class="box">
<div class="img-box" v-if="!showLoading">
<img :src="src" alt="" />
</div>
</div>
<Loading v-if="showLoading"></Loading>
</template>
效果一样可以出来, 接下来就利用指令的方式来优化
V-loading 指令实现
思路:
- 在dom挂载完成后, 创建Loading实例, 需要挂载到写在具体指令结构上
- loading需要知道传递的显示文字, 这里通过指令动态的参数传递
- 当loading组件参数更新后卸载, 关闭loading
1. 使用自定义指令的参数
和内置指令类似,自定义指令的参数也可以是动态的, 下面是Vue官网的截图
在模版中使用 v-loading:[title]="showLoading"
const title = ref('华为加载中...')
<template>
<div class="box" v-loading:[title]="showLoading">
...
</div>
<!-- <Loading v-if="showLoading"></Loading> -->
</template>
2. 利用插件注册指令
在Loading文件下创建js文件
import Loading from './index.vue' // 导入.vue文件
const loadingDirective = {
}
export default loadingDirective
components下创建index.js文件
import loading from '@/components/Loading/index'
export default {
install: (app: App) => {
app.directive('loading', loading)
}
入口文件中注册插件
import MyLoading from '@/components/index'
app.use(MyLoading)
3. 指令 - 节点都挂载完成后调用
- createApp 作用: 创建一个应用实例 - 创建loading
- app.mount 作用: 将应用实例挂载在一个容器元素中
- mounted 参数 el=>获取dom
- mounted 参数 binding.value => 控制开启和关闭loding 也就是 showLoading
- mounted 参数 binding.arg => loading显示的文字 [例如华为加载中...]
const loadingDirective = {
/* 节点都挂载完成后调用 */
mounted(el: any, binding: DirectiveBinding) {
/*
value 控制开启和关闭loding
arg loading显示的文字
*/
const { value, arg } = binding
/* 创建loading实例,并挂载 */
const app = createApp(Loading)
// 这一步 instance === loading.vue
// 此时就可以视同loading.vue 也就是组件实例的方法和属性
const instance = app.mount(document.createElement('div'))
/* 为了让elAppend获取到创建的div元素 */
el.instance = instance
/* 如果传入了自定义的文字就添加title */
if (arg) {
instance.setTitle(arg)
}
/* 如果showLoading为true将loading实例挂载到指令元素中 */
if (value) {
// 添加方法方法, 看下文
handleAppend(el)
}
},
}
可以从控制台查看binding中的title以及showLoading的值
instance.setTitle(arg) 这里既然使用到了组件实例的setTitle方法, 就需要在loading中对应的方法
注意: 在vue3中需要利用defineExpose抛出事件, 让外界可以访问或使用
Loading.vue
const title = ref('')
const setTitle = (val: string) => {
title.value = val
}
// defineProps({ 组件通讯就使用不到了, 注释即可
// title: {
// type: String,
// default: '正在加载中...'
// }
// })
defineExpose({
setTitle,
title
})
<template>
<div class="loading-box">
<div class="loading-content">
<img src="./loading.gif" />
<p class="desc">{{ title }}</p>
</div>
</div>
</template>
4. 指令 - handleAppend(el)方法实现
/* 将loading添加到指令所在DOM */
const handleAppend = (el: any) => {
console.log(el.instance.$el, 'el.instance.$el')
el.appendChild(el.instance.$el)
}
5. 指令 - updated() 更新后挂载还是消除的逻辑
在第四步中, loading已经可以通过指令显示了, 此时还需要让showLoading为false的时候, 或者这么理解: 当新的值不等于老值的是够关闭loading
此时就可以利用指令中updated钩子去执行这一段关闭的逻辑, 一下是官网的说明
/* 更新后调用 */
updated(el: any, binding: DirectiveBinding) {
const { value, oldValue, arg } = binding
if (value !== oldValue) {
/* 更新标题 */
if (arg) {
el.instance.setTitle(arg)
}
// 是显示吗? 如果是就添加 : 如果不是就删除
value ? handleAppend(el) : c(el)
}
}
6. 指令 - handleRemove()方法实现
/* 将loading在DOM中移除 */
const handleRemove = (el: any) => {
removeClass(el, relative as any)
el.removeChild(el.instance.$el)
}
此时基本已经完成了需求, 但是上文我提到了坑点, 原因是loading是通过绝对定位的方式进行水平居中, 那么比如我要在图片中显示loading呢? 我们来实现下这个坑点
7. 坑点的说明
<template>
<div class="box" //关闭 v-loading:[title]="showLoading">
<div class="img-box" v-loading:[title]="showLoading">
<img :src="src" alt="" />
</div>
</div>
<!-- <Loading v-if="true"></Loading> -->
</template>
很明显发现, 执行现在图片这个盒子上, 并没有水平居中, 审查元素其实也很明显, css样式中是根据子绝父相, 但是此时大盒子并没有提供相对定位, 自然就无法水平居中
那么如何修改呢? 其实只要给绑定指令的盒子添加position: relative;属性即可, 当然absolute或者fixed效果一样可以居中
问题已找到了, 那么在appendChild时判断当前是否存在relative | absolute | fixed 的其中一个, 如果没有就需要classList.add进行添加, 同时在removeChild删除添加的relative | absolute | fixed 即可
8. 完善坑点, 实现水平居中
getComputedStyle() 在MDN上的说明:
方法返回一个对象,该对象在应用活动样式表并解析这些值可能包含的任何基本计算后报告元素的所有 CSS 属性的值。私有的 CSS 属性值可以通过对象提供的 API 或通过简单地使用 CSS 属性名称进行索引来访问。
/* 将loading添加到指令所在DOM */
const relative = 'relative'
const handleAppend = (el: any) => {
const style = getComputedStyle(el)
if (!['absolute', 'relative', 'fixed'].includes(style.position)) {
addClass(el, relative as any)
}
el.appendChild(el.instance.$el)
}
// 添加relative
const addClass = (el: any, className: string) => {
if (!el.classList.contains(className)) {
el.classList.add(className)
}
}
// 删除relative
const removeClass = (el: any, className: string) => {
el.classList.remove(className)
}
结尾
夜深人静又是卷到凌晨1点, 只管努力, 其他交给天意~ 旋风冲锋手动撒花 ✿✿ヽ(°▽°)ノ✿
demo地址: gitee.com/tcwty123/v-…
来源:juejin.cn/post/7281825352530296843