众所周知,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>