1、axios监听进度
上传和下载操作在前端中是非常常见的,当我们想知道上传或下载的进度时也不难,axios已经实现了监听进度的方法
import axios from 'axios'
// 上传请求
axios.post('/api/v1/upload', {
data: xxx
},
{
// onUploadProgress回调可以获取进度
onUploadProgress(e) {
const complete = e.loaded / e.total * 100
}
}
)
// 或者
axios({
method: 'POST',
data: {
data: xxx
},
onUploadProgress(e) {
const complete = e.loaded / e.total * 100
}
})
翻一下axios的源码,看看它是如何实现的
在/lib/adapters/xhr.js文件中,可以看到这么一段代码
let request = new XMLHttpRequest()
// Not all browsers support upload events
if (typeof config.onUploadProgress === 'function' && request.upload) {
request.upload.addEventListener('progress', progressEventReducer(config.onUploadProgress));
}
axios是基于XMLHttpRequest来实现的
其中,config就是我们传给的axios的参数,如果是上传操作并且有传递onUploadProgress函数的话
就监听XMLHttpRequest的progress事件,然后周期性地触发回调函数progressEventReducer
再看一下progressEventReducer的实现:
function progressEventReducer(listener, isDownloadStream) {
return e => {
const loaded = e.loaded;
const total = e.lengthComputable ? e.total : undefined;
...
const data = {
loaded,
total,
progress: total ? (loaded / total) : undefined,
...
};
data[isDownloadStream ? 'download' : 'upload'] = true; // 区别上传或是下载
listener(data);
};
}
在progressEventReducer中,就会获取
- loaded:已上传的buffer数据流
- total:总的buffer数据流
最终会将数据传给listener,即一开始传给axios的onUploadProgress回调,所以我们可以通过e.loaded / e.total来获取进度
2、不准确的进度条
获取进度条很简单,但是在实际使用中还是碰到了问题
- 计算出来的进度与实际上传进度不符合
- 可以看出:进度条已经走完,但是接口一直在pending中
这个问题还是比较严重的,明明显示完成了上传,但是文件就是没有出来
而且文件越大,差异就越明显
原因
造成这个原因,其实跟TCP协议发送数据的方式有关
- 在客户端,会有一个send buffer,即数据缓存区
- 这个buffer缓存区存储的就是等待发送的数据,tcp协议层会在适当的时候选取一部分数据发送出去
- 当我们上传文件时,数据会被不断地write到这个缓存区;每次写入时就会被侦听到,然后调用一次onUploadProgress,e.loaded其实表示的就是写入到buffer缓存区的数据
- 但是此时的数据可能还没有被发送出去,仍然在缓存区中放着;加上发送数据也需要时间
- 等到数据全部到达Server的时候,才会执行response回调,这时候上传操作才算完成;但客户端那边早就显示发送完毕了,时差也就出来了
3、模拟进度条
如果解决这个问题呢?有一个比较保守的做法:用一个亦真亦假的进度条
在进度达到一定值的时候,开始人为干涉;每次让它走一点,给人一种一直在上传的感觉,直到上传完成,赋值为100即可
这里就使用vue3的方式实现一下:
import { ref } from 'vue'
/**
* 模拟进度条
* @returns
*/
export const useProgress = (name: string) => {
const progress = ref<number>(0)
const timer = ref<number | null>(null)
const onUploadProgress = (e: ProgressEvent) => {
const complete = (e.loaded / e.total) * 100
// 超过80 开始干涉
if (complete >= 80) {
if (timer.value) return
timer.value = window.setInterval(() => {
progress.value += (100 - progress.value) * 0.2 // 每次增加剩下的20%
// 超过99 不再变化(此时也接近上传完成了)
if (progress.value > 99) {
timer.value && window.clearInterval(timer.value)
}, 1000)
} else {
// 在80之前都按照axios计算出的进度来显示
progress.value = complete
}
}
return {
progress,
onUploadProgress
}
}
可以在请求完成的时候,将progress设置为100即可
具体的阈值可以根据情况自行设置
其实,大名鼎鼎的NProgress库也是这么做的,我们平时使用它的姿势是这样的:
NProgress.start() // 开启进度条
NProgress.done() // 进度条结束
可是,又没有监听接口,仅仅只是开启进度条,它怎么能预测我们的进度呢?
答案是:它也是模拟进度条
在其源码中,有这么一段代码:
当我们没有传amount的时候,会自动根据当前进度条的status调整每次的进度amount
当进度staus超过0.994的时候,就会一直停在0.994这个状态
直到我们执行NProgress.done()时,会直接调用NProgress.set(1),进度条走完
参考:
https://github.com/axios/axios
https://github.com/rstacruz/nprogress