当我们刷新页面或者是首次加载的时候, 如果后端数据请求比较慢的情况下; 页面是会出现白屏情况的
所以我们可以使用 v-loading 去优化一下, 增加用户的体验性
我们可以有两种方式去实现:
1. 定义一个 loading 的组件, 然后在每一个需要显示 loading 效果的组件中手动的导入
2. 使用自定义指令(v-loading)去动态的添加 loading 效果
显然第二种方案是要比第一种方案优雅的
如果给每一个需要添加 loading 效果的组件都去手动导入注册的话, 这个行为未免有点繁琐
首先我们需要明确, 我们需要在一个 DOM 元素中插入一个 loading 的效果
且这个 loading 效果并不是一尘不变的, 在当组件数据更新的之后; 我们需要将 loading 效果进行移除
第一步:
创建 loading.vue 公共的基础组件
此组件需要向外暴露一个 setTitle 方法, 用于动态修改 loading 的文案
<template>
<div class="loading">
<div class="loading-content">
<img width="24" height="24" src="./loading.gif">
<p class="desc">{{title}}</p>
</div>
</div>
</template>
<script>
export default {
name: 'loadingCom',
data () {
return {
title: ''
}
},
methods: {
setTitle (title) {
this.title = title
}
}
}
</script>
<style lang="scss" scoped>
.loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate3d(-50%, -50%, 0);
.loading-content {
text-align: center;
.desc {
line-height: 20px;
font-size: $font-size-small;
color: $color-text-l;
}
}
}
</style>
第二步:
vue 提供了一个 api (directive), 用于注册自定义指令
此 api 函数中需要传入两个参数:
1. 自定义指令名称
2. 配置项
所以, 我们还需要创建 v-loading 的配置文件(directive)
然后在 main.js 入口文件中进行注册
// 导入v-loading的配置项
import loadingDirective from '@/components/base/loading/directive'
createApp(App).use(store).use(router).directive('loading', loadingDirective).mount('#app')
第三步:
现在我们就需要去封装 loadingDirective 这一个配置项
1. 首先导入 loading 组件
2. 导入 createApp 函数
3. 定义 loadingDirective 对象, 此对象中有两个成员 mounted(组件挂载时) 和 updated(组件更新时)
4. 先完成 mounted 函数的逻辑
import loading from './loading.vue'
import { createApp } from 'vue'
const loadingDirective = {
mounted (el, binding) {
// 创建根组件
const app = createApp(loading)
// 获取vue实例
const instance = app.mount(document.createElement('div'))
// 因为在添加方法内部需要使用到instance, 但是instance是一个局部变量
// 那么就添加到el对象中
el.instance = instance
if (binding.value) {
append(el)
}
}
}
// 添加元素方法
function append (el) {
el.appendChild(el.instance.$el)
}
5. 组件数据更新的时候, 需要执行 updated 钩子函数
import loading from './loading.vue'
import { createApp } from 'vue'
const loadingDirective = {
mounted (el, binding) {
const app = createApp(loading)
const instance = app.mount(document.createElement('div'))
el.instance = instance
if (binding.value) {
append(el)
}
},
updated (el, binding) {
// 如果旧值不等于新值的话, 就需要动态的执行添加和移除元素方法
if (binding.oldValue !== binding.value) {
// 组件没有再次初始化的前提下, 何时会再次显示loading效果
// 比如, 分页组件, 当当前页码值发生变化的时候需要重新请求数据; 渲染数据
binding.value ? append(el) : remove(el)
}
}
}
function append (el) {
el.appendChild(el.instance.$el)
}
// 移除元素方法
function remove (el) {
el.removeChild(el.instance.$el)
}
export default loadingDirective
配置文件写到这里, 一个 v-loading 自定义指令就完成了
但是我们还可以做一些优化:
1. 不要依赖父元素的定位, 因为 loading 效果需要在父元素的正中间显示
2. 可以动态的设置 loading 效果的文案
先来完成第一个优化
我们将对一些 dom 的操作提取到一个 dom.js 文件中
export function addClass (el, className) {
// 不要对一个元素重复添加class类
if (!el.classList.contains(className)) {
el.classList.add(className)
}
}
export function removeClass (el, className) {
// 如果删除一个不存在的class类是不会报错的
el.classList.remove(className)
}
然后我们需要对 loadingDirective 配置项进行修改
import loading from './loading.vue'
import { createApp } from 'vue'
import { addClass, removeClass } from '@/assets/js/dom'
// 在css中定义一个class类
const relativeCls = 'g-relative'
const loadingDirective = {
mounted (el, binding) {
const app = createApp(loading)
const instance = app.mount(document.createElement('div'))
el.instance = instance
if (binding.value) {
append(el)
}
},
updated (el, binding) {
if (binding.oldValue !== binding.value) {
binding.value ? append(el) : remove(el)
}
}
}
function append (el) {
// 动态的获取当前el元素的style
const style = getComputedStyle(el)
// 判断是否存在以下几种定位
if (['fixed', 'absolute', 'relative'].indexOf(style.position) === -1) {
addClass(el, relativeCls)
}
el.appendChild(el.instance.$el)
}
function remove (el) {
// 组件数据更新的时候需要移除掉class类
removeClass(el, relativeCls)
el.removeChild(el.instance.$el)
}
export default loadingDirective
第二个优化:
在组件挂载的时候, 去动态的给 loading 效果添加文案
这里需要使用到 loading 组件向外暴露的 setTitle 函数
然后通过 binding.arg 方法获取传入的数据, 再调用 setTitle 进行动态的设置
import loading from './loading.vue'
import { createApp } from 'vue'
import { addClass, removeClass } from '@/assets/js/dom'
const relativeCls = 'g-relative'
const loadingDirective = {
mounted (el, binding) {
const app = createApp(loading)
const instance = app.mount(document.createElement('div'))
el.instance = instance
// 获取组件传入的文案数据
const title = binding.arg
if (typeof title !== 'undefined') {
instance.setTitle(title)
}
if (binding.value) {
append(el)
}
},
updated (el, binding) {
// 组件更新的时候也是一样
const title = binding.arg
if (typeof title !== 'undefined') {
el.instance.setTitle(title)
}
if (binding.oldValue !== binding.value) {
binding.value ? append(el) : remove(el)
}
}
}
function append (el) {
const style = getComputedStyle(el)
if (['fixed', 'absolute', 'relative'].indexOf(style.position) === -1) {
addClass(el, relativeCls)
}
el.appendChild(el.instance.$el)
}
function remove (el) {
removeClass(el, relativeCls)
el.removeChild(el.instance.$el)
}
export default loadingDirective
然后看一下在需要 v-loading 的组件使用
<template>
<div v-loading:[loadingText]="loading"></div>
</template>
<script>
export default {
components: { Slider, Scroll },
data () {
loadingText: '正在载入...'
}
},
computed: {
loading () {
return // 动态判断loading效果是否显示
}
}
}
</script>
我们还可以将 loadingDirective 这一个配置项兼容不同的自定义指令, 当然前提就是共用这一个配置项的自定义指令的业务应该是差不多
首先我们得封装一个函数, 这一个函数的返回值是一个对象(也就是配置项)
然后这一个函数需要接收不同的 vue 组件, 来在 el 上生成不同的实例
所以我们需要在 update , append 和 remove 方法执行的时候, 区分 el 上不同的实例
import { createApp } from 'vue'
import { addClass, removeClass } from '@/assets/js/dom'
const relativeCls = 'g-relative'
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)
}
},
updated (el, binding) {
const name = comp.name
const title = binding.arg
if (typeof title !== 'undefined') {
el[name].instance.setTitle(title)
}
if (binding.value !== binding.oldValue) {
binding.value ? append(el) : remove(el)
}
}
}
function append (el) {
const name = comp.name
const style = getComputedStyle(el)
if (['fixed', 'absolute', '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)
}
}
那么 loading/directive.js 中的代码需要做修改
import loading from './loading.vue'
import createLoadingLikeDirective from '@/assets/js/create-loading-like-directive'
export default createLoadingLikeDirective(loading)