造轮子:无进度等待组件

1,107 阅读5分钟

应用场景

在前端开发中会有这种需求:在异步请求的过程页面呈现等待效果。发起请求时,界面上出现一个圆圈在转,旁边有数字实时变化,显示进度的百分比。

轮子的特点

这种场景的特殊之处在于,等待过程中没有任何方式能获得当前进度。与文件分片上传不同,利用当前成功上传的文件片数和总片数可以计算得到进度百分比。而无进度场景下,等待的时长是未知的,当前的进度也是未知的。

等待进度的实现

无进度等待组件中,进度用的是假数据。组件实现的关键是如何制造假数据,我们先用定时器让进度每50ms变化一次,从0计数到100后销毁定时器。

function progress(data) {
    data = 0;
    const loop = setInterval(function () {
        if (data === 100) {
            clearInterval(loop)
           }
        data++
    }, 50)
}

如果响应在计数完成前提前返回了,不能直接退出等待状态,而要让进度达到百分之100后才能结束等待状态。

我们的思路是实现两个定时器。定时器loop在响应返回前计数,响应一旦返回,开启新的定时器overLoop进行计数。

function progress(data, isProgress) {
    data = 0;
    const loop = setInterval(function () {
            if (!isProgress) {
                const overLoop = setInterval(function () {
                    if (data === 100) {
                        clearInterval(overLoop)
                    }
                    data++
                }, 20)
                clearInterval(loop)
            }
       
            if(data === 100) { 
                clearInterval(loop)
            }
            else{
                data++
            }
        }
    }, 50)
}

这里要区分两个状态,isProgress标志异步请求是否进行中,而isLoad标志是否处于等待状态。这两个状态标志是不同的,因为异步请求结束了,等待状态是不能结束的,而必须等到进度达到百分之100后等待状态才能结束。

如果计数完成了响应仍未返回,我们不能直接结束等待状态。这时,我们要让进度卡在百分之99等待响应返回。这种交互方式虽然不能真实的反应进度,但总比直接结束要好的多。

function progress(data, isProgress) {
    data = 0;
    const loop = setInterval(function () {
            if (!isProgress) {
                const overLoop = setInterval(function () {
                    if (data === 100) {
                        clearInterval(overLoop)
                    }
                    data++
                }, 20)
                clearInterval(loop)
            }
       
            if(data === 99) { 
                return
            }
            else{
                data++
            }
        }
    }, 50)
}

在设置两个定时器的循环速度时,第一个定时器的速度要慢,第二个定时器的速度要快。第二个速度要快是为了在响应返回后能够尽快完成交互,缩短无意义的计数。 第一个速度要慢是为了尽量避免进度卡在百分之99的情况,给异步请求更多的时间。

但是,两个定时器的速度不能差距过大,否则给人一种忽慢忽快的印象。实际使用时,两个定时器的速度应该由开发人员根据具体场景进行配置,从而能达到最好的交互效果。

Vue组件的实现

元素和样式

首先,我们需要展示一个等待圆圈和进度数字,在Vue中用element的loading组件实现转圈效果。

<div v-if="isLoad" class="loading">
    <span class="loading-circle" v-loading="true"></span>
    <span>等待中{{data}}%</span>
</div>

loading组件是用svg实现的,可以用transform:scale放大缩小。

.loading-circle {
    display: inline-block;
    transform:scale(0.5);
  }

组件API的设计要求能够对组件的核心属性进行配置,在Vue组件内属性通过prop获得。暴露的属性有:isProgress、tip、scale、speed、overSpeed和loadOverCallback。

  • isProgress: 异步请求的标记, true是进行中,false是响应已返回

  • tip: 提示语

  • scale: 圆圈的缩放比例

  • speed:第一个定时器的间隔

  • overSpeed:第二个定时器的间隔

  • loadOverCallback:计数完成的回调

完整的Vue组件

把以上实现的无进度等待组件封装成一个Vue组件,可以在模板里直接使用。

<template>
  <div v-if="isLoad">
    <span :style="loadingStyle" v-loading="true"></span>
    <span>{{tip}}{{percentage}}%</span>
  </div>
</template>

<script>
import { Loading } from "element";
export default {
  props: {
    isProgress: {
      type: Boolean,
      default: false
    },
    tip: {
      type: String,
      default: "等待中"
    },
    scale: {
      type: Number,
      default: 1
    },
    speed: {
      type: Number,
      default: 50
    },
    overSpeed: {
      type: Number,
      default: 20
    },
    //计数结束的回调
    loadOverCallback: {
      type: Function
    }
  },

  watch: {
    isProgress: function(newVal, oldVal) {
      //异步开始
      if (newVal && !oldVal) {
        this.init();
      }
    },

    isLoad: function(newVal, oldVal) {
      //计数结束
      if (oldVal && !newVal) {
        if (loadOverCallback) {
          loadOverCallback.call();
        }
      }
    }
  },

  data() {
    return {
      isLoad: false,
      percentage: 0,
      loadingStyle: "display: inline-block;transform: scale(" + scale + ");"
    };
  },
  methods: {
    init() {
      this.percentage = 0;  
      this.isLoad = true;
      this.progress();
    },
    progress() {
      const _that = this;
      const loop = setInterval(function() {
        //异步结束
        if (!_that.isProgress) {
          const overLoop = setInterval(function() {
            if (_that.percentage === 100) {
              //计数结束
              _that.isLoad = false;
              clearInterval(overLoop);
            }
            _that.percentage++;
          }, overSpeed);

          clearInterval(loop);
        }

        if (progress.data === 99) {
          //如果计数到99,而外部数据仍未返回,则卡在99
          return;
        } else {
          _that.percentage++;
        }
      }, speed);
    }
  }
};
</script>

模块化组件的实现

实现一个基于ES6模块的无进度等待组件,在组件的js文件中暴露一个入口函数。

export default function progressComponent(progress, isLoadProxy, speed, overSpeed) {
    //speed和overSpeed作为参数传入,控制计数的速度
    //progress对象是引用类型,组件内部修改其属性可以反映到外部。
    progress.data = 0;

    const loop = setInterval(function () {
        if (!progress.isProgress) {
            const overLoop = setInterval(function () {
                if (progress.data === 100) {
                    isLoadProxy.isLoad = false //外部监听isLoad的变化
                    clearInterval(overLoop)
                }
                progress.data++
            }, overSpeed)
            clearInterval(loop)
        }

        if (progress.data === 99) {
            //如果计数到100,而外部数据仍未返回,则卡在99
            return
        }
        else {
            progress.data++
        }
    }, speed)
}

isLoadProxy通过Proxy让外面能够监听到组件内部状态的变化,从而改变isLoad。

import progressComponent from "../path"

let progress = { data:0, isProgress:false },
    isLoadProxy = { isLoad:true },
    handler = {
        set: function(target, key, value, receiver) {
          if (key==='isLoad'&&value === false) {
            this.isLoad = false;  //改变外部的isLoad状态
          }
          return true;
        }
      };
      
//异步请求发起
//start async

//调用不定时等待组件
progress.isProgress = true;
progressComponent(progress, new Proxy(isLoadProxy, handler), 50, 20);