赞
踩
众所周知,loading有指令和服务两种实现,写指令实现的时候遇到了很多困难。首先是指令要根据生命周期钩子,vue3的钩子和vue2不一样了。再者是指令传值的解决方案,vue官方文档上写的例子有一定的省略,有一定的误导倾向。这些先不谈,先一步步从最简单开始。
首先作出一个组件,把这个组件调用,能显示出loading的画面。代码如下,其中css中的var是定义的全局css变量,相关代码未展示,可以换成一个固定的颜色比如#000,即可
<template> <div class="loading-mask"> <div class="loading-spinner "> <div class="loading"></div> </div> </div> </template> <style scoped> .loading-mask { position: absolute; z-index: 2000; background-color: hsla(0,0%,100%,.9); margin: 0; top: 0; right: 0; bottom: 0; left: 0; transition: opacity .3s; } .loading-spinner { position: absolute; height: 50px; top: 50%; margin-top: -21px; width: 100%; text-align: center; } .loading { display: inline-block; width: 50px; perspective: 200px; } .loading:before, .loading:after { position: absolute; width: 20px; height: 20px; content: ""; animation: jumping 0.5s infinite alternate; background: rgba(0, 0, 0, 0); } .loading:before { left: 0; } .loading:after { right: 0; animation-delay: 0.15s; } @keyframes jumping { 0% { transform: scale(1) translateY(0px) rotateX(0deg); box-shadow: 0 0 0 rgba(0, 0, 0, 0); } 100% { transform: scale(1.2) translateY(-25px) rotateX(45deg); background: var(--main-color); box-shadow: 0 25px 40px #000; } } </style>
效果如下
成功显示在了页面的正中间居中,是css中的位置absolute和 text-align: center;的功劳。
为什么要这么写居中呢,是为了后面一个需求的考虑,我们要实现loading页面的某一部分,比如页面中有三个div,我们只想loading其中一个,就需要定位,只需要给那个需要的元素的css position改为relative即可。
这里我们就先实现指令模式,在html中添加一个指令,名为v-laoding。被加的那个div就是我们想要让它loading的那个。但是loading完了总要结束吧,所以要给loading传值,传入的是true,就显示,false就是代表loading结束了。
所以我们的测试代码就写好了
<template> <div> <div class="zz"> 1 </div> <div class="zz" v-loading="'loading'"> 2 </div> <div class="zz"> 3 </div> </div> </template> <style scoped> .zz{ width: 500px; height: 500px; } </style>
这样一个demo放进去会报错,因为loading指令,也就是v-loading没部署,接下来部署一下。我们部署要做两件事,第一件:放入新的dom元素(loading),第二件:给加指令的那个div添加新的样式(css position改为relative)
然后考虑一下这两件事要不要提出公共方法,第一件不用以为加dom就一句话的事情,更多的是判断啥时候要加,这个判断运用不到未来可能会添加的代码中,是逻辑不同的。所以第一件不用。第二件则要提取出来,添加class名,和去除class名,可以提取成2个方法,入参是class名字。于是先再公共utils里加入这个公共方法。
// 往dom元素中添加class
export function addClass(el, className) {
if (!el.classList.contains(className)) {
el.classList.add(className)
}
}
export function removeClass(el, className) {
el.classList.remove(className)
}
接下来考虑vue3的函数式编程思路,考虑要不要将添加指令的js文件进行函数化拆分。要的。拆成两个,一个是构造参数部分,一个是添加部分。
// directive.js
import Loading from './loading.vue'
import createLoadingLikeDirective from './create-loading-like-directive'
const loadingDirective = createLoadingLikeDirective(Loading)
export default loadingDirective
// create-loading-like-directive.js import { createApp } from 'vue' import { addClass, removeClass } from '@/sanorin/utils/dom' const relativeCls = 'sanorin-loading-parent--relative' // 这个样式的代码写在全局样式里,所谓全局样式,对标的是elementui使用的时候在main.js中要引入的三段话中的css那段。 //如下: //.sanorin-loading-parent--relative{ // position: relative !important; //} export default function createLoadingLikeDirective(Comp) { return { mounted(el, binding) { console.log(el, binding) const app = createApp(Comp) console.log(app) const instance = app.mount(document.createElement('div')) console.log(instance) const name = Comp.name if (!el[name]) { el[name] = {} } el[name].instance = instance const title = binding.arg if (typeof title !== 'undefined') { instance.setTitle(title) } append(el) }, updated(el, binding) { const title = binding.arg const name = Comp.name if (typeof title !== 'undefined') { el[name].instance.setTitle(title) } if (binding.value !== binding.oldValue) { console.log(binding.value) binding.value ? append(el) : remove(el) } } } function append(el) { const name = Comp.name const style = getComputedStyle(el) console.log(style.position) if (['absolute', 'fixed', 'relative'].indexOf(style.position) === -1) { addClass(el, relativeCls) } el.appendChild(el[name].instance.$el) } function remove(el) { const name = Comp.name removeClass(el, relativeCls) el.removeChild(el[name].instance.$el) } }
之后把directive.js在main.js中使用即可,有注释部分为新添加部分
import { createApp } from 'vue'
import App from './App.vue'
import loadingDirective from './sanorin/packages/loading/directive' // 引入指令函数
import './sanorin/style/global.css' // 引入组件库全局样式
const app = createApp(App)
app.directive('loading', loadingDirective) // 使用指令函数,注意要在#app之前
app.mount('#app')
我们上面的测试demo效果如下
因为我们以前部署了了组件库的use,于是把关于这个组件库的指令,也放进去,相关代码看以前的文章,应该是这个系列的第一篇。代码如下,有注释的为新加的loading指令相关代码
import menu from './packages/menu/menu.vue'; import exhibitFrame from './packages/exhibit-frame/exhibit-frame.vue'; import button from './packages/button/button.vue'; import input from './packages/input/input.vue'; import radio from './packages/radio/radio.vue'; import radioGroup from './packages/radio/radio-group.vue'; import loadingDirective from './packages/loading/directive' // 引入指令 const components = [menu,exhibitFrame,button,input,radio,radioGroup] const sanorin = { install: (app:any) => { components.forEach(component => { app.component(component.name, component) }) app.directive('loading', loadingDirective) // 部署指令 } } export default sanorin;
接下来完善一下,加入可自定义的提示语
想要用指令中的arg来进行提示语的传递,详见vue官方文档
还是先写测试demo
<script setup> import { ref } from 'vue' let loadingFlag = ref(true) setInterval(() => { loadingFlag.value = !loadingFlag.value }, 2000); </script> <template> <div class="zz" v-loading:[`拼命加载中……`]="loadingFlag"> 2 </div> </template> <style scoped> .zz{ width: 200px; height: 200px; border: 1px solid red; } </style>
前面部署loading的时候已经写了进去setTitle方法,现在要在组件里真正把这个方法写上
<script setup> import { ref } from "vue" let title = ref('') let setTitle = (e) => title.value = e defineExpose({ // 使用 <script setup> 的组件是默认关闭的——即通过模板引用或者 $parent 链获取到的组件的公开实例,不会暴露任何在 <script setup> 中声明的绑定。可以通过 defineExpose 编译器宏来显式指定在 <script setup> 组件中要暴露出去的属性 setTitle }) </script> <template> <div class="loading-mask"> <div class="loading-spinner "> <div class="dec">{{title}}</div> <div class="loading"></div> </div> </div> </template> <style scoped> .loading-mask { position: absolute; z-index: 2000; background-color: hsla(0,0%,100%,.9); margin: 0; top: 0; right: 0; bottom: 0; left: 0; transition: opacity .3s; } .loading-spinner { position: absolute; height: 50px; top: 50%; margin-top: -21px; width: 100%; text-align: center; } .dec { transform: translateY(-25px) } .loading { display: inline-block; width: 50px; perspective: 200px; } .loading:before, .loading:after { position: absolute; width: 20px; height: 20px; content: ""; animation: jumping 0.5s infinite alternate; background: rgba(0, 0, 0, 0); } .loading:before { left: 0; } .loading:after { right: 0; animation-delay: 0.15s; } @keyframes jumping { 0% { transform: scale(1) translateY(0px) rotateX(0deg); box-shadow: 0 0 0 rgba(0, 0, 0, 0); } 100% { transform: scale(1.2) translateY(-25px) rotateX(45deg); background: var(--main-color); box-shadow: 0 25px 40px #000; } } </style>
接下来写全屏,注释中是完整写法,这里有三种传值(即flag,fullscreen,提示语arg)只有一种的话简略的写法都不一样,测试案例:
<script setup> import { ref } from 'vue' let loadingFlag = ref(true) </script> <template> <sanorin-exhibit-frame :header="header" :subHeader="subHeader" :metaTitle="metaTitle" :metaCode="metaCode"> <!-- <div class="zz" v-loading:[`年后`].fullscreen="loadingFlag"> --> <div class="zz" v-loading.fullscreen="loadingFlag"> 2 </div> </sanorin-exhibit-frame> </template> <style scoped> .zz{ width: 200px; height: 200px; border: 1px solid red; } </style>
部署指令的mounted中的if(binding.value)判断中加一个
if (binding.modifiers.fullscreen) {
addClass(el.lastChild, fullscreenCls)
}
至此指令部分就完事了,最后整理一下,除了测试demo文件,有以下文件
1、loading.vue 组件文件
<script setup> import { ref } from "vue" let title = ref('') let setTitle = (e) => title.value = e defineExpose({ setTitle }) </script> <template> <div class="loading-mask"> <div class="loading-spinner "> <div class="dec">{{title}}</div> <div class="loading"></div> </div> </div> </template> <style scoped> .loading-mask { position: absolute; z-index: 2000; background-color: hsla(0,0%,100%,.9); margin: 0; top: 0; right: 0; bottom: 0; left: 0; transition: opacity .3s; } .loading-spinner { position: absolute; height: 50px; top: 50%; margin-top: -21px; width: 100%; text-align: center; } .dec { transform: translateY(-25px) } .loading { display: inline-block; width: 50px; perspective: 200px; } .loading:before, .loading:after { position: absolute; width: 20px; height: 20px; content: ""; animation: jumping 0.5s infinite alternate; background: rgba(0, 0, 0, 0); } .loading:before { left: 0; } .loading:after { right: 0; animation-delay: 0.15s; } @keyframes jumping { 0% { transform: scale(1) translateY(0px) rotateX(0deg); box-shadow: 0 0 0 rgba(0, 0, 0, 0); } 100% { transform: scale(1.2) translateY(-25px) rotateX(45deg); background: var(--main-color); box-shadow: 0 25px 40px #000; } } </style>
2、create-loading-like-directive.js 指令构造文件
import { createApp } from 'vue' import { addClass, removeClass } from '@/sanorin/utils/dom' const relativeCls = 'sanorin-loading-parent--relative' const fullscreenCls = 'sanorin-loading-parent--fullscreen' export default function createLoadingLikeDirective(Comp) { return { mounted(el, binding) { const app = createApp(Comp) const instance = app.mount(document.createElement('div')) const name = Comp.name if (!el[name]) { el[name] = {} } el[name].instance = instance const title = binding.arg if (typeof title !== 'undefined') { instance.setTitle(title) } if (binding.value) { append(el) if (binding.modifiers.fullscreen) { addClass(el.lastChild, fullscreenCls) } } }, updated(el, binding) { const title = binding.arg const name = Comp.name if (typeof title !== 'undefined') { el[name].instance.setTitle(title) } if (binding.value !== binding.oldValue) { console.log(binding.value) binding.value ? append(el) : remove(el) } if (binding.value) { if (binding.modifiers.fullscreen) { addClass(el.lastChild, fullscreenCls) } } } } function append(el) { const name = Comp.name const style = getComputedStyle(el) console.log(style.position) if (['absolute', 'fixed', 'relative'].indexOf(style.position) === -1) { addClass(el, relativeCls) } el.appendChild(el[name].instance.$el) } function remove(el) { const name = Comp.name removeClass(el, relativeCls) el.removeChild(el[name].instance.$el) } }
3、directive.js 指令部署文件
import Loading from './loading.vue'
import createLoadingLikeDirective from './create-loading-like-directive'
const loadingDirective = createLoadingLikeDirective(Loading)
export default loadingDirective
4、global.css 组件库样式文件中添加
.sanorin-loading-parent--relative{
position: relative !important;
}
.sanorin-loading-parent--fullscreen{
position: fixed !important;
}
5、组件注册文件中添加
import loadingDirective from './packages/loading/directive'
....
app.directive('loading', loadingDirective)
接着写loading的服务方式调用
先写测试demo;
<script setup> import { sanorinLoading } from '@/sanorin/packages/loading/service' // 因为没npm打包,后续改成import { sanorinLoading } from 'sanorinUI' sanorinLoading.show(); setTimeout(() => { sanorinLoading.hide() }, 5000) </script> <template> <sanorin-exhibit-frame :header="header" :subHeader="subHeader" :metaTitle="metaTitle" :metaCode="metaCode"> <div class="zz"> 2 </div> </sanorin-exhibit-frame> </template> <style scoped> .zz{ width: 200px; height: 200px; border: 1px solid red; } </style>
指令同级下新建一个service.js
import { createApp, ref } from 'vue' import myLoad from './loading.vue' const titleService = ref('') const $loading = createApp(myLoad, { titleService }).mount(document.createElement('div')) const sanorinLoading = { show(e) { titleService.value = e document.body.appendChild($loading.$el) }, hide() { titleService.value = '' document.body.removeChild($loading.$el) } } export { sanorinLoading }
最后loading组件也要相应改一下
<script setup> import { computed, ref } from "vue" // service const props = defineProps({ titleService: { type: String }, }) // directive let titleDirective = ref('') let setTitle = (e) => titleDirective.value = e defineExpose({ setTitle }) let title = computed(() => props.titleService?.value || titleDirective.value) </script> <template> <div class="loading-mask"> <div class="loading-spinner "> <div class="dec">{{title}}</div> <div class="loading"></div> </div> </div> </template> <style scoped> .loading-mask { position: absolute; z-index: 2000; background-color: hsla(0,0%,100%,.9); margin: 0; top: 0; right: 0; bottom: 0; left: 0; transition: opacity .3s; } .loading-spinner { position: absolute; height: 50px; top: 50%; margin-top: -21px; width: 100%; text-align: center; } .dec { transform: translateY(-25px) } .loading { display: inline-block; width: 50px; perspective: 200px; } .loading:before, .loading:after { position: absolute; width: 20px; height: 20px; content: ""; animation: jumping 0.5s infinite alternate; background: rgba(0, 0, 0, 0); } .loading:before { left: 0; } .loading:after { right: 0; animation-delay: 0.15s; } @keyframes jumping { 0% { transform: scale(1) translateY(0px) rotateX(0deg); box-shadow: 0 0 0 rgba(0, 0, 0, 0); } 100% { transform: scale(1.2) translateY(-25px) rotateX(45deg); background: var(--main-color); box-shadow: 0 25px 40px #000; } } </style>
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。