引言
你是否曾好奇过,当你在网页上传一个巨大的假期视频时,那个平滑移动的进度条是怎么知道已经上传了多少的?今天我们就来扒一扒这个看似简单实则暗藏玄机的功能——文件上传进度监控。👀
上传进度条:用户体验的一剂"安慰剂"?
老实说,第一次实现上传进度监控时,我还挺兴奋的。想象一下,用户上传一个500MB的文件,没有任何反馈,他们可能会怀疑人生:"是上传了吗?系统卡住了?我是不是该刷新页面?"——然后失去所有上传进度,重新开始这个痛苦的过程。
进度条就像是告诉用户"别急,我在努力工作呢"的小助手。它可能不会让上传速度变快,但至少能让用户知道:系统没有死机,他们的文件正在一点一点地爬向服务器。
揭开进度监控的神秘面纱
XMLHttpRequest 的 upload 对象
进度监控的核心其实是浏览器内置的一个看起来有点"老古董"的API——XMLHttpRequest(简称XHR)。别被它的名字骗了,虽然带个"XML",但它早已超越了处理XML的范畴,成为了处理各种HTTP请求的瑞士军刀。
XHR有个特殊的属性叫upload,这个玩意能告诉我们文件上传的进度信息。
// 这段代码可能是你见过最简单的进度监控实现了
const xhr = new XMLHttpRequest();
xhr.open('POST', 'https://example.com/upload');
xhr.upload.onprogress = function(event) {
if (event.lengthComputable) {
// 这里的 event.loaded 是已上传的字节数
// event.total 是文件总大小
const percentComplete = (event.loaded / event.total) * 100;
console.log(`已上传 ${percentComplete.toFixed(1)}%`);
// 想象这里更新了一个进度条的宽度
}
};
xhr.send(formData); // formData 中包含要上传的文件
就是这么简单!我第一次看到这段代码时简直不敢相信——原来监控上传进度只需要这么几行代码?!
axios 的 onUploadProgress
如今,直接使用XHR的开发者越来越少了。我们更喜欢用axios这样的现代HTTP客户端库。axios提供了一个超级方便的选项onUploadProgress,让我们可以轻松监控上传进度:
axios.post('https://example.com/upload', formData, {
onUploadProgress: (progressEvent) => {
const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);
console.log(`上传进度: ${percentCompleted}%`);
// 这里可以更新进度条UI
}
});
看起来很简洁对吧?但这背后究竟发生了什么?axios是如何实现这个功能的?我带着这个问题,决定扒一扒axios的源码。
深入axios源码:onUploadProgress 的真相
我第一次看axios源码的时候,脑子里想的是:"这肯定是个非常复杂的实现,可能用了各种黑科技..."
然后我找到了关键代码(在lib/adapters/xhr.js文件中),结果发现,axios的实现比我想象的简单多了:
// axios源码中处理上传进度的部分(简化版)
if (typeof config.onUploadProgress === 'function' && request.upload) {
request.upload.addEventListener('progress', config.onUploadProgress);
}
就...这么简单?!没错,axios只是把我们传入的onUploadProgress回调函数直接绑定到了XMLHttpRequest的upload.progress事件上!这简直就像是你请了个代购,结果他只是把你的订单原封不动地转发了一下。😂
但话说回来,这也体现了良好API设计的哲学——不要过度设计,简单直接才是王道。axios并没有试图重新发明轮子,而是巧妙地封装了浏览器原生能力,让开发者能够以更现代、更一致的方式使用它。
上传进度事件的生命周期
如果把文件上传比作一场马拉松,那么进度事件就是沿途的打卡点。从起点到终点,XMLHttpRequest的upload对象会触发一系列事件:
- loadstart:选手出发!上传开始了。
- progress:选手正在奔跑(这个事件会被频繁触发,就像沿途的摄像头不断拍下选手的英姿)。
- error:哎呀,选手摔倒了!上传出错。
- abort:选手中途退赛了(用户取消了上传)。
- load:胜利!选手冲过终点线,上传成功完成。
- timeout:超时!选手没能在规定时间内完成比赛。
- loadend:无论成功还是失败,比赛都结束了。
作为开发者,我们最关心的通常是第2个事件——progress,因为它让我们能够追踪"选手"(数据)已经跑了多远。
进度监控的真实面目:有时候会"撒谎"
说起来有点扎心,但不得不承认:进度条有时候会"撒谎"。它显示的进度并不一定是服务器实际接收到的数据量,而是浏览器认为已经发送出去的数据量。
想象一下这个场景:你上传了一个大文件,进度条显示99%,但突然网络断开了。实际上服务器可能只收到了60%的数据,但浏览器已经把数据"发送"出去了(或者认为发送出去了),所以进度条欢快地跑到了99%。
这就是为什么有时候明明进度条已经走完,服务器却返回了错误——数据在传输过程中丢失了,但浏览器并不知情。
实战经验:处理真实世界的复杂性
在我实际开发中,发现了几个处理上传进度的小技巧,分享给各位:
1. 别让进度条跳得太频繁
浏览器触发进度事件的频率可能非常高,特别是在高速网络环境下。如果每次事件都更新UI,可能会导致页面卡顿。所以,我通常会使用节流(throttle)来限制更新频率:
javascript
复制
let lastUpdateTime = 0;
axios.post('/upload', formData, {
onUploadProgress: (event) => {
const now = Date.now();
// 每100毫秒更新一次UI,避免过于频繁的DOM操作
if (now - lastUpdateTime > 100) {
const percent = Math.round((event.loaded / event.total) * 100);
updateProgressBar(percent); // 更新UI
lastUpdateTime = now;
}
}
});
2. 速度计算:让用户知道还要等多久
仅仅显示百分比有时候不够直观。用户真正关心的可能是:"我还要等多久?"计算上传速度和估计剩余时间可以大大提升用户体验:
javascript
复制
let startTime = Date.now();
let lastLoaded = 0;
let lastTime = startTime;
axios.post('/upload', formData, {
onUploadProgress: (event) => {
const currentTime = Date.now();
const elapsedSeconds = (currentTime - startTime) / 1000;
// 计算平均速度(字节/秒)
const averageSpeed = event.loaded / elapsedSeconds;
// 计算瞬时速度
const instantSpeed = (event.loaded - lastLoaded) / ((currentTime - lastTime) / 1000);
// 估计剩余时间
const remainingBytes = event.total - event.loaded;
const estimatedSeconds = remainingBytes / averageSpeed;
// 更新UI
updateProgressInfo({
percent: Math.round((event.loaded / event.total) * 100),
speed: formatSpeed(instantSpeed), // 例如 "1.2 MB/s"
remaining: formatTime(estimatedSeconds) // 例如 "约2分钟"
});
lastLoaded = event.loaded;
lastTime = currentTime;
}
});
这样用户就能看到"已完成30%,当前速度1.2MB/s,预计剩余2分钟"这样的信息,是不是比单纯的进度条友好多了?
3. 处理无法计算进度的情况
有时候,服务器没有发送Content-Length头,或者由于其他原因,event.lengthComputable为false,此时我们无法计算百分比进度。这种情况下,我可以显示已上传的字节数:
javascript
复制
onUploadProgress: (event) => {
if (event.lengthComputable) {
// 显示百分比
showPercentProgress(Math.round((event.loaded / event.total) * 100));
} else {
// 只显示已上传的数据量
showUploadedSize(formatSize(event.loaded));
}
}
结语
上传进度监控的原理并不复杂——依赖浏览器提供的XMLHttpRequest的upload.progress事件,而axios的onUploadProgress只是对这一机制的简单封装。下一篇文章我们来介绍使用SSE实现文件进度监控。