当我们刷新页面或者是首次加载的时候, 如果后端数据请求比较慢的情况下; 页面是会出现白屏情况的
所以我们可以使用 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)