应用场景
在前端开发中会有这种需求:在异步请求的过程页面呈现等待效果。发起请求时,界面上出现一个圆圈在转,旁边有数字实时变化,显示进度的百分比。
轮子的特点
这种场景的特殊之处在于,等待过程中没有任何方式能获得当前进度。与文件分片上传不同,利用当前成功上传的文件片数和总片数可以计算得到进度百分比。而无进度场景下,等待的时长是未知的,当前的进度也是未知的。
等待进度的实现
无进度等待组件中,进度用的是假数据。组件实现的关键是如何制造假数据,我们先用定时器让进度每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);